use crate::attachments::{Attachment, AttachmentSource, InlineKeyboard};
use crate::client::MaxClient;
use crate::error::Result;
use crate::utils;
use serde_json::json;
use std::time::Duration;
#[derive(Debug, Clone, Default)]
pub struct SendMessageParams {
pub chat_id: Option<i64>,
pub user_id: Option<i64>,
pub text: String,
pub format: Option<String>,
pub notify: Option<bool>,
pub disable_link_preview: Option<bool>,
pub reply_mid: Option<String>,
pub attachments: Vec<Attachment>,
pub max_retries: usize,
}
pub struct SendMessageParamsBuilder {
chat_id: Option<i64>,
user_id: Option<i64>,
text: String,
format: Option<String>,
notify: Option<bool>,
disable_link_preview: Option<bool>,
reply_mid: Option<String>,
attachments: Vec<Attachment>,
max_retries: usize,
}
impl SendMessageParamsBuilder {
pub fn new() -> Self {
Self {
text: String::new(),
chat_id: None,
user_id: None,
format: None,
notify: Some(true),
disable_link_preview: None,
reply_mid: None,
attachments: vec![],
max_retries: 3,
}
}
pub fn text(mut self, text: impl Into<String>) -> Self {
self.text = text.into();
self
}
pub fn chat_id(mut self, id: i64) -> Self {
self.chat_id = Some(id);
self
}
pub fn user_id(mut self, id: i64) -> Self {
self.user_id = Some(id);
self
}
pub fn format_markdown(mut self) -> Self {
self.format = Some("markdown".to_string());
self
}
pub fn format_html(mut self) -> Self {
self.format = Some("html".to_string());
self
}
pub fn silent(mut self) -> Self {
self.notify = Some(false);
self
}
pub fn disable_preview(mut self) -> Self {
self.disable_link_preview = Some(true);
self
}
pub fn reply_to(mut self, mid: impl Into<String>) -> Self {
self.reply_mid = Some(mid.into());
self
}
pub fn attachment(mut self, att: Attachment) -> Self {
self.attachments.push(att);
self
}
pub fn attachments(mut self, atts: Vec<Attachment>) -> Self {
self.attachments = atts;
self
}
pub fn max_retries(mut self, retries: usize) -> Self {
self.max_retries = retries;
self
}
pub fn build(self) -> SendMessageParams {
SendMessageParams {
chat_id: self.chat_id,
user_id: self.user_id,
text: self.text,
format: self.format,
notify: self.notify,
disable_link_preview: self.disable_link_preview,
reply_mid: self.reply_mid,
attachments: self.attachments,
max_retries: self.max_retries,
}
}
}
impl Default for SendMessageParamsBuilder {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Default)]
pub struct GetMessagesParams {
pub chat_id: i64,
pub marker: Option<String>,
pub count: Option<u32>,
}
#[derive(Debug, Clone, Default)]
pub struct EditMessageParams {
pub text: Option<String>,
pub format: Option<String>,
pub attachments: Vec<Attachment>,
pub inline_keyboard: Option<InlineKeyboard>,
pub disable_link_preview: Option<bool>,
}
pub(crate) async fn prepare_attachments_json(
client: &MaxClient,
attachments: &[Attachment],
) -> Result<serde_json::Value> {
let mut attachments_json = Vec::new();
for att in attachments {
match att {
Attachment::Image { source, token, url } => {
if let Some(url_str) = url {
attachments_json.push(serde_json::json!({
"type": "image",
"payload": { "url": url_str }
}));
} else {
let final_token = if let Some(tok) = token {
tok.clone()
} else {
match source {
AttachmentSource::LocalFile(path) => {
let typ = att.get_type();
let tok = client.upload_file(path, typ).await?;
tokio::time::sleep(Duration::from_millis(500)).await;
tok
}
AttachmentSource::Token(tok) => tok.clone(),
}
};
attachments_json.push(serde_json::json!({
"type": "image",
"payload": { "token": final_token }
}));
}
}
Attachment::Video { source, token, url, filename } |
Attachment::Audio { source, token, url, filename } |
Attachment::File { source, token, url, filename } => {
if let Some(url_str) = url {
let file_name = filename.as_deref().ok_or_else(|| {
crate::error::Error::InvalidInput("Missing filename for URL attachment".into())
})?;
let temp_path = client.download_to_temp_dir(url_str, file_name).await?;
let typ = att.get_type();
let token = client.upload_file(&temp_path, typ).await?;
tokio::time::sleep(Duration::from_millis(500)).await;
let _ = tokio::fs::remove_file(&temp_path).await;
let _ = tokio::fs::remove_dir(temp_path.parent().unwrap()).await;
attachments_json.push(serde_json::json!({
"type": typ,
"payload": { "token": token }
}));
} else {
let final_token = if let Some(tok) = token {
tok.clone()
} else {
match source {
AttachmentSource::LocalFile(path) => {
let typ = att.get_type();
let tok = client.upload_file(path, typ).await?;
tokio::time::sleep(Duration::from_millis(500)).await;
tok
}
AttachmentSource::Token(tok) => tok.clone(),
}
};
attachments_json.push(serde_json::json!({
"type": att.get_type(),
"payload": { "token": final_token }
}));
}
}
Attachment::Sticker { code } => {
attachments_json.push(serde_json::json!({
"type": "sticker",
"payload": { "code": code }
}));
}
Attachment::Contact(data) => {
let mut payload = serde_json::Map::new();
if let Some(name) = &data.name {
payload.insert("name".into(), name.clone().into());
}
if let Some(contact_id) = data.contact_id {
payload.insert("contact_id".into(), contact_id.into());
}
if let Some(vcf_info) = &data.vcf_info {
payload.insert("vcf_info".into(), vcf_info.clone().into());
}
if let Some(vcf_phone) = &data.vcf_phone {
payload.insert("vcf_phone".into(), vcf_phone.clone().into());
}
attachments_json.push(serde_json::json!({
"type": "contact",
"payload": payload
}));
}
Attachment::InlineKeyboard(keyboard) => {
let mut buttons_json = Vec::new();
for row in &keyboard.buttons {
let mut row_json = Vec::new();
for btn in row {
let mut btn_json = serde_json::Map::new();
btn_json.insert("type".into(), btn.r#type.clone().into());
btn_json.insert("text".into(), btn.text.clone().into());
if let Some(payload) = &btn.payload {
btn_json.insert("payload".into(), payload.clone().into());
}
if let Some(url) = &btn.url {
btn_json.insert("url".into(), url.clone().into());
}
if let Some(quick) = btn.quick {
btn_json.insert("quick".into(), quick.into());
}
if let Some(web_app) = &btn.web_app {
btn_json.insert("web_app".into(), web_app.clone().into());
}
if let Some(contact_id) = btn.contact_id {
btn_json.insert("contact_id".into(), contact_id.into());
}
if let Some(app_payload) = &btn.app_payload {
btn_json.insert("payload".into(), app_payload.clone().into());
}
if let Some(msg_text) = &btn.message_text {
btn_json.insert("text".into(), msg_text.clone().into());
}
row_json.push(serde_json::Value::Object(btn_json));
}
buttons_json.push(serde_json::Value::Array(row_json));
}
attachments_json.push(serde_json::json!({
"type": "inline_keyboard",
"payload": { "buttons": buttons_json }
}));
}
Attachment::Location { latitude, longitude } => {
attachments_json.push(serde_json::json!({
"type": "location",
"payload": { "latitude": latitude, "longitude": longitude }
}));
}
Attachment::Share(data) => {
let mut payload = serde_json::Map::new();
if let Some(url) = &data.url {
payload.insert("url".into(), url.clone().into());
}
if let Some(token) = &data.token {
payload.insert("token".into(), token.clone().into());
}
attachments_json.push(serde_json::json!({
"type": "share",
"payload": payload
}));
}
}
}
Ok(serde_json::Value::Array(attachments_json))
}
impl MaxClient {
pub async fn send_message(&self, params: SendMessageParams) -> Result<Vec<String>> {
if (params.chat_id.is_none() && params.user_id.is_none()) ||
(params.chat_id.is_some() && params.user_id.is_some()) {
return Err(crate::error::Error::InvalidInput(
"Exactly one of chat_id or user_id must be specified".into(),
));
}
let mut ready_attachments = Vec::new(); let mut content_attachments = Vec::new(); let mut keyboard_attachments = Vec::new();
for att in ¶ms.attachments {
match att {
Attachment::Image { source, token, url } => {
let final_token = if let Some(tok) = token {
tok.clone()
} else {
match source {
AttachmentSource::LocalFile(path) => {
let file_type = att.get_type();
let tok = self.upload_file(path, file_type).await?;
tokio::time::sleep(Duration::from_millis(500)).await; tok
}
AttachmentSource::Token(tok) => tok.clone(),
}
};
if let Some(url_str) = url {
content_attachments
.push((att.get_type(), json!({ "payload": { "url": url_str } })));
} else {
ready_attachments.push((att.get_type(), final_token));
}
}
Attachment::Video { source, token, url, filename } |
Attachment::Audio { source, token, url, filename } |
Attachment::File { source, token, url, filename } => {
if let Some(url_str) = url {
let file_name = filename.as_deref().ok_or_else(|| {
crate::error::Error::InvalidInput("Missing filename for URL attachment".into())
})?;
let temp_path = self.download_to_temp_dir(url_str, file_name).await?;
let typ = att.get_type();
let token = self.upload_file(&temp_path, typ).await?;
tokio::time::sleep(Duration::from_millis(500)).await;
let _ = tokio::fs::remove_file(&temp_path).await;
let _ = tokio::fs::remove_dir(temp_path.parent().unwrap()).await;
ready_attachments.push((typ, token));
} else {
let final_token = if let Some(tok) = token {
tok.clone()
} else {
match source {
AttachmentSource::LocalFile(path) => {
let typ = att.get_type();
let tok = self.upload_file(path, typ).await?;
tokio::time::sleep(Duration::from_millis(500)).await;
tok
}
AttachmentSource::Token(tok) => tok.clone(),
}
};
ready_attachments.push((att.get_type(), final_token));
}
}
Attachment::Sticker { code } => {
let payload = json!({ "code": code });
content_attachments.push((att.get_type(), json!({ "payload": payload })));
}
Attachment::Contact(data) => {
let mut payload = serde_json::Map::new();
if let Some(name) = &data.name {
payload.insert("name".into(), name.clone().into());
}
if let Some(contact_id) = data.contact_id {
payload.insert("contact_id".into(), contact_id.into());
}
if let Some(vcf_info) = &data.vcf_info {
payload.insert("vcf_info".into(), vcf_info.clone().into());
}
if let Some(vcf_phone) = &data.vcf_phone {
payload.insert("vcf_phone".into(), vcf_phone.clone().into());
}
content_attachments.push((att.get_type(), json!({ "payload": payload })));
}
Attachment::InlineKeyboard(keyboard) => {
let mut buttons_json = Vec::new();
for row in &keyboard.buttons {
let mut row_json = Vec::new();
for btn in row {
let mut btn_json = serde_json::Map::new();
btn_json.insert("type".into(), btn.r#type.clone().into());
btn_json.insert("text".into(), btn.text.clone().into());
if let Some(payload) = &btn.payload {
btn_json.insert("payload".into(), payload.clone().into());
}
if let Some(url) = &btn.url {
btn_json.insert("url".into(), url.clone().into());
}
if let Some(quick) = btn.quick {
btn_json.insert("quick".into(), quick.into());
}
if let Some(web_app) = &btn.web_app {
btn_json.insert("web_app".into(), web_app.clone().into());
}
if let Some(contact_id) = btn.contact_id {
btn_json.insert("contact_id".into(), contact_id.into());
}
if let Some(app_payload) = &btn.app_payload {
btn_json.insert("payload".into(), app_payload.clone().into());
}
if let Some(msg_text) = &btn.message_text {
btn_json.insert("text".into(), msg_text.clone().into());
}
row_json.push(serde_json::Value::Object(btn_json));
}
buttons_json.push(serde_json::Value::Array(row_json));
}
let payload = json!({ "buttons": buttons_json });
keyboard_attachments.push((att.get_type(), json!({ "payload": payload })));
}
Attachment::Location { latitude, longitude } => {
let payload = json!({ "latitude": latitude, "longitude": longitude });
content_attachments.push((att.get_type(), payload));
}
Attachment::Share(data) => {
let mut payload = serde_json::Map::new();
if let Some(url) = &data.url {
payload.insert("url".into(), url.clone().into());
}
if let Some(token) = &data.token {
payload.insert("token".into(), token.clone().into());
}
content_attachments.push((att.get_type(), json!({ "payload": payload })));
}
}
}
let fragments = utils::split_text(¶ms.text, params.format.as_deref());
let fragments = if fragments.is_empty() &&
ready_attachments.is_empty() &&
content_attachments.is_empty()
{
return Err(crate::error::Error::InvalidInput(
"Empty message text and no attachments".into(),
));
} else if fragments.is_empty() {
vec!["".to_string()]
} else {
fragments
};
let mut get_params = Vec::new();
if let Some(disable) = params.disable_link_preview {
get_params.push(("disable_link_preview", disable.to_string()));
}
let reply_link = params
.reply_mid
.as_ref()
.map(|mid| json!({ "type": "reply", "mid": mid }));
let mut sent_ids = Vec::new();
for (i, fragment_text) in fragments.iter().enumerate() {
let mut body = json!({
"text": fragment_text,
});
if let Some(fmt) = ¶ms.format {
body["format"] = json!(fmt);
}
if let Some(notify) = params.notify {
body["notify"] = json!(notify);
}
if i == 0 {
if !body["attachments"].is_array() {
body["attachments"] = serde_json::Value::Array(vec![]);
}
let attachments_array = body["attachments"].as_array_mut().unwrap();
for (att_type, token) in &ready_attachments {
attachments_array.push(json!({
"type": att_type,
"payload": { "token": token }
}));
}
for (att_type, payload) in &content_attachments {
let mut attachment = json!({ "type": att_type });
if let Some(p) = payload.as_object() {
for (k, v) in p {
attachment[k] = v.clone();
}
} else {
attachment["payload"] = payload.clone();
}
attachments_array.push(attachment);
}
if let Some(link) = &reply_link {
body["link"] = link.clone();
}
}
if i == fragments.len() - 1 && !keyboard_attachments.is_empty() {
if !body["attachments"].is_array() {
body["attachments"] = serde_json::Value::Array(vec![]);
}
let attachments_array = body["attachments"].as_array_mut().unwrap();
for (att_type, payload) in &keyboard_attachments {
let mut attachment = json!({ "type": att_type });
if let Some(p) = payload.as_object() {
for (k, v) in p {
attachment[k] = v.clone();
}
} else {
attachment["payload"] = payload.clone();
}
attachments_array.push(attachment);
}
}
match self.send_fragment_with_retry(
params.chat_id,
params.user_id,
body,
&get_params,
i == 0, params.max_retries,
).await {
Ok(mid) => sent_ids.push(mid),
Err(e) => {
if !sent_ids.is_empty() {
let _ = self.delete_messages(&sent_ids).await;
}
return Err(e);
}
}
}
Ok(sent_ids)
}
pub async fn send_message_builder(
&self,
builder: SendMessageParamsBuilder,
) -> Result<Vec<String>> {
self.send_message(builder.build()).await
}
pub async fn forward_message(
&self,
chat_id: Option<i64>,
user_id: Option<i64>,
forward_mid: &str,
notify: Option<bool>,
disable_link_preview: Option<bool>,
) -> Result<String> {
if (chat_id.is_none() && user_id.is_none()) ||
(chat_id.is_some() && user_id.is_some()) {
return Err(crate::error::Error::InvalidInput(
"Exactly one of chat_id or user_id must be specified".into(),
));
}
let mut query = Vec::new();
if let Some(cid) = chat_id {
query.push(("chat_id", cid.to_string()));
}
if let Some(uid) = user_id {
query.push(("user_id", uid.to_string()));
}
if let Some(disable) = disable_link_preview {
query.push(("disable_link_preview", disable.to_string()));
}
query.push(("message_id", forward_mid.to_string()));
let body = json!({
"link": {
"type": "forward",
"mid": forward_mid
},
"notify": notify.unwrap_or(true),
});
let resp: serde_json::Value = self.request_with_rate_limit(
reqwest::Method::POST,
"/messages",
&query,
Some(body),
).await?;
let new_mid = resp["message"]["body"]["mid"]
.as_str()
.ok_or_else(|| crate::error::Error::Api {
code: 500,
message: "No message ID in forward response".into(),
})?;
Ok(new_mid.to_string())
}
pub async fn delete_message(&self, message_id: &str) -> Result<()> {
self.request_with_rate_limit::<serde_json::Value>(
reqwest::Method::DELETE,
"/messages",
&[("message_id", message_id.to_string())],
None,
).await?;
Ok(())
}
pub async fn delete_messages(&self, message_ids: &[String]) -> Result<()> {
for id in message_ids {
if let Err(e) = self.delete_message(id).await {
eprintln!("Warning: failed to delete message {}: {}", id, e);
}
}
Ok(())
}
pub async fn get_messages(
&self,
params: GetMessagesParams,
) -> Result<(Vec<crate::types::Message>, Option<String>)> {
let mut query = vec![("chat_id", params.chat_id.to_string())];
if let Some(m) = ¶ms.marker {
query.push(("marker", m.clone()));
}
if let Some(c) = params.count {
query.push(("count", c.to_string()));
}
let resp: serde_json::Value = self
.request_with_rate_limit(reqwest::Method::GET, "/messages", &query, None)
.await?;
let messages: Vec<crate::types::Message> =
serde_json::from_value(resp["messages"].clone())?;
let next_marker = resp
.get("marker")
.and_then(|v| v.as_str())
.map(String::from);
Ok((messages, next_marker))
}
pub async fn edit_message(&self, message_mid: &str, params: EditMessageParams) -> Result<()> {
let mut body = serde_json::Map::new();
if let Some(text) = params.text {
body.insert("text".into(), text.into());
}
if let Some(format) = params.format {
body.insert("format".into(), format.into());
}
if let Some(disable) = params.disable_link_preview {
body.insert("disable_link_preview".into(), disable.into());
}
let mut all_attachments = params.attachments;
if let Some(keyboard) = params.inline_keyboard {
all_attachments.push(Attachment::InlineKeyboard(keyboard));
}
if !all_attachments.is_empty() {
let attachments_json = prepare_attachments_json(self, &all_attachments).await?;
body.insert("attachments".into(), attachments_json);
}
self.request_with_rate_limit::<serde_json::Value>(
reqwest::Method::PUT,
"/messages",
&[("message_id", message_mid.to_string())],
Some(serde_json::Value::Object(body)),
)
.await?;
Ok(())
}
pub async fn edit_multimessage(
&self,
chat_id: i64,
old_mids: &[String],
new_text: &str,
format: Option<&str>,
notify: Option<bool>,
disable_link_preview: Option<bool>,
reply_mid: Option<&str>,
attachments: Vec<Attachment>,
inline_keyboard: Option<InlineKeyboard>,
) -> Result<Vec<String>> {
let fragments = utils::split_text(new_text, format);
if fragments.is_empty() {
return Err(crate::error::Error::InvalidInput(
"Empty message text after splitting".into(),
));
}
let old_count = old_mids.len();
let new_count = fragments.len();
let first_attachments_json = if !attachments.is_empty() {
Some(prepare_attachments_json(self, &attachments).await?)
} else {
None
};
let keyboard_attachment_json = if let Some(keyboard) = inline_keyboard {
let kb_att = Attachment::InlineKeyboard(keyboard);
let json_arr = prepare_attachments_json(self, &[kb_att]).await?;
if let serde_json::Value::Array(mut arr) = json_arr {
arr.pop()
} else {
None
}
} else {
None
};
let mut new_ids = Vec::with_capacity(new_count);
for i in 0..std::cmp::min(old_count, new_count) {
let mut body = serde_json::Map::new();
body.insert("text".into(), fragments[i].clone().into());
if let Some(fmt) = format {
body.insert("format".into(), fmt.into());
}
if let Some(disable) = disable_link_preview {
body.insert("disable_link_preview".into(), disable.into());
}
let mut final_attachments = Vec::new();
if i == 0 {
if let Some(ref arr) = first_attachments_json {
if let serde_json::Value::Array(arr_vec) = arr {
final_attachments.extend(arr_vec.clone());
}
}
}
if i == new_count - 1 {
if let Some(kb) = &keyboard_attachment_json {
final_attachments.push(kb.clone());
}
}
if !final_attachments.is_empty() {
body.insert("attachments".into(), serde_json::Value::Array(final_attachments));
}
let query = [("message_id", old_mids[i].to_string())];
self.request_with_rate_limit::<serde_json::Value>(
reqwest::Method::PUT,
"/messages",
&query,
Some(serde_json::Value::Object(body)),
)
.await?;
new_ids.push(old_mids[i].clone());
}
if old_count > new_count {
let extra_mids = &old_mids[new_count..];
self.delete_messages(extra_mids).await?;
}
if new_count > old_count {
let extra_fragments = &fragments[old_count..];
for (i, text) in extra_fragments.iter().enumerate() {
let mut builder = SendMessageParamsBuilder::new()
.text(text)
.chat_id(chat_id)
.max_retries(3);
if let Some(fmt) = format {
if fmt == "markdown" {
builder = builder.format_markdown();
} else if fmt == "html" {
builder = builder.format_html();
}
}
if let Some(notify_val) = notify {
if !notify_val {
builder = builder.silent();
}
}
if let Some(disable) = disable_link_preview {
if disable {
builder = builder.disable_preview();
}
}
if i == 0 && reply_mid.is_some() {
builder = builder.reply_to(reply_mid.unwrap());
}
let sent_ids = self.send_message_builder(builder).await?;
new_ids.extend(sent_ids);
}
}
Ok(new_ids)
}
async fn send_fragment_with_retry(
&self,
chat_id: Option<i64>,
user_id: Option<i64>,
body: serde_json::Value,
get_params: &[(&str, String)],
is_first: bool,
max_retries: usize,
) -> Result<String> {
let mut attempt = 0;
let mut delay_sec = 1; let mut rate_limit_delay_sec = 5;
loop {
match self
.send_single_fragment(chat_id, user_id, body.clone(), get_params)
.await
{
Ok(mid) => return Ok(mid),
Err(e) => {
if !is_first {
return Err(e);
}
attempt += 1;
if attempt >= max_retries {
return Err(e);
}
let err_str = e.to_string();
if err_str.contains("429") || err_str.contains("too.many.requests") {
tokio::time::sleep(Duration::from_secs(rate_limit_delay_sec)).await;
rate_limit_delay_sec *= 2;
continue;
}
if err_str.contains("attachment.not.ready") || err_str.contains("not.processed") {
tokio::time::sleep(Duration::from_secs(delay_sec)).await;
delay_sec *= 2;
continue;
}
return Err(e);
}
}
}
}
async fn send_single_fragment(
&self,
chat_id: Option<i64>,
user_id: Option<i64>,
body: serde_json::Value,
get_params: &[(&str, String)],
) -> Result<String> {
let mut query = get_params.to_vec();
if let Some(cid) = chat_id {
query.push(("chat_id", cid.to_string()));
}
if let Some(uid) = user_id {
query.push(("user_id", uid.to_string()));
}
let resp: serde_json::Value = self.request_with_rate_limit(
reqwest::Method::POST,
"/messages",
&query,
Some(body),
).await?;
let mid = resp["message"]["body"]["mid"]
.as_str()
.ok_or_else(|| crate::error::Error::Api {
code: 500,
message: "No message ID in response (expected message.body.mid)".into(),
})?;
Ok(mid.to_string())
}
}