Skip to content

Commit

Permalink
Add send_photo
Browse files Browse the repository at this point in the history
  • Loading branch information
lifegpc authored Sep 20, 2024
1 parent 995ea62 commit cd8b2a7
Show file tree
Hide file tree
Showing 7 changed files with 393 additions and 2 deletions.
13 changes: 13 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ chrono = { version = "0.4", features = ["serde"] }
dateparser = "0.2.0"
derive_builder = "0.20"
derive_more = "0.99"
derive_setters = "0.1"
fancy-regex = "0.11"
flagset = { version = "0.4", optional = true }
futures-util = "0.3"
Expand Down
135 changes: 135 additions & 0 deletions src/formdata.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
use derive_builder::Builder;
use derive_setters::Setters;
use reqwest::header::HeaderMap;
use reqwest::multipart::{Form, Part};
use std::path::{Path, PathBuf};

#[derive(Debug, derive_more::From)]
pub enum FormDataBody {
Data(Vec<u8>),
File(PathBuf),
}

#[derive(Builder, Debug, Setters)]
#[builder(pattern = "owned", setter(strip_option))]
#[setters(borrow_self, into)]
/// Part of form
pub struct FormDataPart {
#[builder(default, setter(into))]
#[setters(strip_option)]
/// Mime type
mime: Option<String>,
#[builder(default, setter(into))]
#[setters(strip_option)]
/// File name
filename: Option<String>,
#[builder(default)]
#[setters(skip)]
/// HTTP headers
pub headers: HeaderMap,
#[setters(skip)]
#[builder(setter(into))]
/// Body
body: FormDataBody,
}

/// Form
pub struct FormData {
fields: Vec<(String, FormDataPart)>,
}

/// Error when convert [FormData] to [Form]
#[derive(Debug, derive_more::Display, derive_more::From)]
pub enum FormDataError {
IOError(std::io::Error),
Reqwest(reqwest::Error),
}

impl FormData {
pub fn new() -> Self {
Self::default()
}

pub fn data<'a, K: AsRef<str> + ?Sized, V: AsRef<[u8]> + ?Sized>(
&'a mut self,
key: &K,
value: &V,
) -> &'a mut FormDataPart {
let part = FormDataPartBuilder::default()
.body(FormDataBody::Data(value.as_ref().to_vec()))
.build()
.unwrap();
self.fields.push((key.as_ref().to_owned(), part));
&mut self.fields.last_mut().unwrap().1
}

pub fn file<'a, K: AsRef<str> + ?Sized, P: AsRef<Path> + ?Sized>(
&'a mut self,
key: &K,
path: &P,
) -> &'a mut FormDataPart {
let part = FormDataPartBuilder::default()
.body(FormDataBody::File(path.as_ref().to_owned()))
.build()
.unwrap();
self.fields.push((key.as_ref().to_owned(), part));
&mut self.fields.last_mut().unwrap().1
}

pub fn part<'a, K: AsRef<str> + ?Sized>(
&'a mut self,
key: &K,
part: FormDataPart,
) -> &'a mut FormDataPart {
self.fields.push((key.as_ref().to_owned(), part));
&mut self.fields.last_mut().unwrap().1
}

pub async fn to_form(&self) -> Result<Form, FormDataError> {
let mut f = Form::new();
for (k, v) in self.fields.iter() {
let mut part = match &v.body {
FormDataBody::Data(d) => Part::bytes(d.clone()),
FormDataBody::File(f) => Part::bytes(tokio::fs::read(f).await?),
};
match &v.mime {
Some(m) => {
part = part.mime_str(m)?;
}
None => {}
}
match &v.filename {
Some(f) => {
part = part.file_name(f.clone());
}
None => {}
}
part = part.headers(v.headers.clone());
f = f.part(k.clone(), part);
}
Ok(f)
}
}

impl Default for FormData {
fn default() -> Self {
Self { fields: Vec::new() }
}
}

#[proc_macros::async_timeout_test(120s)]
#[tokio::test(flavor = "multi_thread")]
async fn test_formdata() {
let p = Path::new("./test");
if !p.exists() {
let re = std::fs::create_dir("./test");
assert!(re.is_ok() || p.exists());
}
std::fs::write("test/formdata.txt", "Good job!").unwrap();
let mut f = FormData::new();
f.data("test", "test2").filename("test.txt");
f.file("test2", "test/formdata.txt")
.filename("formdata.txt")
.mime("text/plain");
f.to_form().await.unwrap();
}
1 change: 1 addition & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ mod exif;
mod ext;
mod fanbox;
mod fanbox_api;
mod formdata;
mod i18n;
mod list;
mod log_cfg;
Expand Down
144 changes: 144 additions & 0 deletions src/push/telegram/botapi_client.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
use super::tg_type::*;
use crate::formdata::FormData;
#[cfg(test)]
use crate::formdata::FormDataPartBuilder;
use crate::webclient::WebClient;
use derive_builder::Builder;
use serde::{Deserialize, Serialize};
Expand Down Expand Up @@ -44,6 +47,100 @@ impl BotapiClient {
}
}

pub async fn send_photo(
&self,
chat_id: &ChatId,
message_thread_id: Option<i64>,
photo: InputFile,
caption: Option<&str>,
parse_mode: Option<ParseMode>,
show_caption_above_media: Option<bool>,
has_spoiler: Option<bool>,
disable_notification: Option<bool>,
protect_content: Option<bool>,
message_effect_id: Option<&str>,
reply_parameters: Option<&ReplyParameters>,
) -> Result<BotApiResult<Message>, BotapiClientError> {
let mut form = FormData::new();
form.data("chat_id", &chat_id.to_string());
match message_thread_id {
Some(m) => {
form.data("message_thread_id", &m.to_string());
}
None => {}
}
match photo {
InputFile::URL(u) => {
form.data("photo", &u);
}
InputFile::Content(c) => {
form.part("photo", c);
}
}
match caption {
Some(c) => {
form.data("caption", c);
}
None => {}
}
match parse_mode {
Some(p) => {
form.data("parse_mode", p.as_ref());
}
None => {}
}
match show_caption_above_media {
Some(p) => {
form.data("show_caption_above_media", &p.to_string());
}
None => {}
}
match has_spoiler {
Some(p) => {
form.data("has_spoiler", &p.to_string());
}
None => {}
}
match disable_notification {
Some(d) => {
form.data("disable_notification", &d.to_string());
}
None => {}
}
match protect_content {
Some(p) => {
form.data("protect_content", &p.to_string());
}
None => {}
}
match message_effect_id {
Some(m) => {
form.data("message_effect_id", m);
}
None => {}
}
match reply_parameters {
Some(r) => {
form.data("reply_parameters", serde_json::to_string(r)?.as_str());
}
None => {}
}
let re = self
.client
.post_multipart(
format!("{}/bot{}/sendPhoto", self.cfg.base, self.cfg.token),
None,
form,
)
.await
.ok_or("Failed to send message.")?;
let status = re.status();
match re.text().await {
Ok(t) => Ok(serde_json::from_str(t.as_str())?),
Err(e) => Err(format!("HTTP ERROR {}: {}", status, e))?,
}
}

pub async fn send_message<T: AsRef<str> + ?Sized>(
&self,
chat_id: &ChatId,
Expand Down Expand Up @@ -174,3 +271,50 @@ async fn test_telegram_botapi_sendmessage() {
}
}
}

#[proc_macros::async_timeout_test(120s)]
#[tokio::test(flavor = "multi_thread")]
async fn test_telegram_botapi_sendphoto() {
match std::env::var("TGBOT_TOKEN") {
Ok(token) => match std::env::var("TGBOT_CHATID") {
Ok(c) => {
let cfg = BotapiClientConfigBuilder::default()
.token(token)
.build()
.unwrap();
let client = BotapiClient::new(&cfg);
let cid = ChatId::try_from(c).unwrap();
let pb = std::path::PathBuf::from("./testdata/夏のチマメ隊🏖️_91055644_p0.jpg");
let c = FormDataPartBuilder::default()
.body(pb)
.filename("夏のチマメ隊🏖️_91055644_p0.jpg")
.mime("image/jpeg")
.build()
.unwrap();
client
.send_photo(
&cid,
None,
InputFile::Content(c),
Some("test.test.test"),
None,
None,
Some(true),
None,
None,
None,
None,
)
.await
.unwrap()
.unwrap();
}
Err(_) => {
println!("No chat id specified, skip test.")
}
},
Err(_) => {
println!("No tg bot token specified, skip test.")
}
}
}
14 changes: 12 additions & 2 deletions src/push/telegram/tg_type.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use crate::formdata::FormDataPart;
use derive_builder::Builder;
use derive_more::From;
use serde::{Deserialize, Serialize};
Expand Down Expand Up @@ -139,7 +140,7 @@ pub struct ReplyParameters {
/// Optional. If the message to be replied to is from a different chat,
/// unique identifier for the chat or username of the channel (in the format `@channelusername`).
/// Not supported for messages sent on behalf of a business account.
#[builder(default)]
#[builder(default, setter(into))]
#[serde(skip_serializing_if = "Option::is_none")]
chat_id: Option<ChatId>,
/// Optional. Pass True if the message should be sent even if the specified message
Expand All @@ -153,7 +154,7 @@ pub struct ReplyParameters {
/// The quote must be an exact substring of the message to be replied to,
/// including bold, italic, underline, strikethrough, spoiler, and custom_emoji entities.
/// The message will fail to send if the quote isn't found in the original message.
#[builder(default)]
#[builder(default, setter(into))]
#[serde(skip_serializing_if = "Option::is_none")]
quote: Option<String>,
/// Optional. Mode for parsing entities in the quote. See formatting options for more details.
Expand All @@ -166,6 +167,15 @@ pub struct ReplyParameters {
quote_position: Option<i64>,
}

#[derive(Debug, derive_more::From)]
/// Represents the contents of a file
pub enum InputFile {
/// URL
URL(String),
/// File data
Content(FormDataPart),
}

#[test]
fn test_chat_id() {
assert_eq!(
Expand Down
Loading

0 comments on commit cd8b2a7

Please sign in to comment.