Add telegram botapi sendMessage

This commit is contained in:
2024-09-20 03:29:43 +00:00
committed by GitHub
parent b15bf70fa6
commit 995ea625a8
7 changed files with 447 additions and 5 deletions

80
Cargo.lock generated
View File

@@ -358,6 +358,41 @@ dependencies = [
"cfg-if", "cfg-if",
] ]
[[package]]
name = "darling"
version = "0.20.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989"
dependencies = [
"darling_core",
"darling_macro",
]
[[package]]
name = "darling_core"
version = "0.20.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5"
dependencies = [
"fnv",
"ident_case",
"proc-macro2",
"quote",
"strsim",
"syn 2.0.68",
]
[[package]]
name = "darling_macro"
version = "0.20.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806"
dependencies = [
"darling_core",
"quote",
"syn 2.0.68",
]
[[package]] [[package]]
name = "dateparser" name = "dateparser"
version = "0.2.1" version = "0.2.1"
@@ -381,6 +416,37 @@ dependencies = [
"syn 1.0.109", "syn 1.0.109",
] ]
[[package]]
name = "derive_builder"
version = "0.20.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd33f37ee6a119146a1781d3356a7c26028f83d779b2e04ecd45fdc75c76877b"
dependencies = [
"derive_builder_macro",
]
[[package]]
name = "derive_builder_core"
version = "0.20.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7431fa049613920234f22c47fdc33e6cf3ee83067091ea4277a3f8c4587aae38"
dependencies = [
"darling",
"proc-macro2",
"quote",
"syn 2.0.68",
]
[[package]]
name = "derive_builder_macro"
version = "0.20.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4abae7035bf79b9877b779505d8cf3749285b80c43941eda66604841889451dc"
dependencies = [
"derive_builder_core",
"syn 2.0.68",
]
[[package]] [[package]]
name = "derive_more" name = "derive_more"
version = "0.99.18" version = "0.99.18"
@@ -867,6 +933,12 @@ dependencies = [
"cc", "cc",
] ]
[[package]]
name = "ident_case"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
[[package]] [[package]]
name = "idna" name = "idna"
version = "0.5.0" version = "0.5.0"
@@ -1534,6 +1606,7 @@ dependencies = [
"chrono", "chrono",
"cmake", "cmake",
"dateparser", "dateparser",
"derive_builder",
"derive_more", "derive_more",
"fancy-regex", "fancy-regex",
"flagset", "flagset",
@@ -1735,6 +1808,7 @@ dependencies = [
"js-sys", "js-sys",
"log", "log",
"mime", "mime",
"mime_guess",
"native-tls", "native-tls",
"once_cell", "once_cell",
"percent-encoding", "percent-encoding",
@@ -1998,6 +2072,12 @@ dependencies = [
"quote", "quote",
] ]
[[package]]
name = "strsim"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]] [[package]]
name = "syn" name = "syn"
version = "1.0.109" version = "1.0.109"

View File

@@ -14,6 +14,7 @@ c_fixed_string = { version = "0.2", optional = true }
cfg-if = "1" cfg-if = "1"
chrono = { version = "0.4", features = ["serde"] } chrono = { version = "0.4", features = ["serde"] }
dateparser = "0.2.0" dateparser = "0.2.0"
derive_builder = "0.20"
derive_more = "0.99" derive_more = "0.99"
fancy-regex = "0.11" fancy-regex = "0.11"
flagset = { version = "0.4", optional = true } flagset = { version = "0.4", optional = true }
@@ -43,11 +44,11 @@ percent-encoding = "*"
proc_macros = { path = "proc_macros" } proc_macros = { path = "proc_macros" }
rand = { version = "0", optional = true } rand = { version = "0", optional = true }
regex = "1" regex = "1"
reqwest = { version = "0.11", features = ["brotli", "deflate", "gzip", "socks", "stream"] } reqwest = { version = "0.11", features = ["brotli", "deflate", "gzip", "multipart", "socks", "stream"] }
rusqlite = { version = "0.29", features = ["bundled", "chrono"], optional = true } rusqlite = { version = "0.29", features = ["bundled", "chrono"], optional = true }
RustyXML = "0.3" RustyXML = "0.3"
serde = "1" serde = "1"
serde_json = { version = "1", optional = true } serde_json = "1"
serde_urlencoded = { version = "*", optional = true } serde_urlencoded = { version = "*", optional = true }
tokio = { version = "1.27", features = ["rt", "macros", "rt-multi-thread", "time"] } tokio = { version = "1.27", features = ["rt", "macros", "rt-multi-thread", "time"] }
url = "2.3" url = "2.3"
@@ -66,7 +67,7 @@ db_all = ["db", "db_sqlite"]
db_sqlite = ["rusqlite"] db_sqlite = ["rusqlite"]
docker = [] docker = []
exif = ["bindgen", "c_fixed_string", "cmake", "link-cplusplus", "utf16string"] exif = ["bindgen", "c_fixed_string", "cmake", "link-cplusplus", "utf16string"]
server = ["async-trait", "base64", "db", "hex", "hyper", "multipart", "openssl", "serde_json", "rand", "serde_urlencoded"] server = ["async-trait", "base64", "db", "hex", "hyper", "multipart", "openssl", "rand", "serde_urlencoded"]
ugoira = ["avdict", "bindgen", "cmake", "link-cplusplus"] ugoira = ["avdict", "bindgen", "cmake", "link-cplusplus"]
[patch.crates-io] [patch.crates-io]

View File

@@ -3,6 +3,5 @@ pub enum SqliteError {
DbError(rusqlite::Error), DbError(rusqlite::Error),
DatabaseVersionTooNew, DatabaseVersionTooNew,
UserNameAlreadyExists, UserNameAlreadyExists,
#[cfg(feature = "serde_json")]
SerdeError(serde_json::Error), SerdeError(serde_json::Error),
} }

View File

@@ -29,7 +29,6 @@ pub enum PixivDownloaderError {
ParseIntError(std::num::ParseIntError), ParseIntError(std::num::ParseIntError),
ReqwestError(reqwest::Error), ReqwestError(reqwest::Error),
PixivAppError(crate::pixivapp::error::PixivAppError), PixivAppError(crate::pixivapp::error::PixivAppError),
#[cfg(feature = "serde_json")]
SerdeJsonError(serde_json::Error), SerdeJsonError(serde_json::Error),
#[cfg(feature = "serde_urlencoded")] #[cfg(feature = "serde_urlencoded")]
SerdeUrlencodedError(serde_urlencoded::ser::Error), SerdeUrlencodedError(serde_urlencoded::ser::Error),

View File

@@ -0,0 +1,176 @@
use super::tg_type::*;
use crate::webclient::WebClient;
use derive_builder::Builder;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
fn default_base() -> String {
String::from("https://api.telegram.org")
}
#[derive(Builder, Clone, Debug, Deserialize, Serialize)]
/// Bot API Client Config
pub struct BotapiClientConfig {
#[serde(default = "default_base")]
#[builder(default = "String::from(\"https://api.telegram.org\")")]
/// Bot API Server. Default: https://api.telegram.org
pub base: String,
/// Auth token
pub token: String,
}
pub struct BotapiClient {
cfg: BotapiClientConfig,
client: WebClient,
}
#[derive(Debug, derive_more::Display, derive_more::From)]
pub enum BotapiClientError {
SerdeJson(serde_json::Error),
Str(String),
}
impl From<&str> for BotapiClientError {
fn from(value: &str) -> Self {
Self::Str(value.to_owned())
}
}
impl BotapiClient {
pub fn new(cfg: &BotapiClientConfig) -> Self {
Self {
cfg: cfg.clone(),
client: WebClient::default(),
}
}
pub async fn send_message<T: AsRef<str> + ?Sized>(
&self,
chat_id: &ChatId,
message_thread_id: Option<i64>,
text: &T,
parse_mode: Option<ParseMode>,
link_preview_options: Option<&LinkPreviewOptions>,
disable_notification: Option<bool>,
protect_content: Option<bool>,
message_effect_id: Option<&str>,
reply_parameters: Option<&ReplyParameters>,
) -> Result<BotApiResult<Message>, BotapiClientError> {
let mut params = HashMap::new();
params.insert("chat_id", chat_id.to_string());
match message_thread_id {
Some(m) => {
params.insert("message_thread_id", m.to_string());
}
None => {}
}
params.insert("text", text.as_ref().to_owned());
match parse_mode {
Some(m) => {
params.insert("parse_mode", m.as_ref().to_owned());
}
None => {}
}
match link_preview_options {
Some(m) => {
params.insert("link_preview_options", serde_json::to_string(m)?);
}
None => {}
}
match disable_notification {
Some(b) => {
params.insert("disable_notification", b.to_string());
}
None => {}
}
match protect_content {
Some(b) => {
params.insert("protect_content", b.to_string());
}
None => {}
}
match message_effect_id {
Some(b) => {
params.insert("message_effect_id", b.to_owned());
}
None => {}
}
match reply_parameters {
Some(b) => {
params.insert("reply_parameters", serde_json::to_string(b)?);
}
None => {}
}
let re = self
.client
.post(
format!("{}/bot{}/sendMessage", self.cfg.base, self.cfg.token),
None,
Some(params),
)
.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))?,
}
}
}
#[proc_macros::async_timeout_test(120s)]
#[tokio::test(flavor = "multi_thread")]
async fn test_telegram_botapi_sendmessage() {
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 data = client
.send_message(
&cid,
None,
"Hello world.",
None,
None,
None,
None,
None,
None,
)
.await
.unwrap()
.unwrap();
let r = ReplyParametersBuilder::default()
.message_id(data.message_id)
.build()
.unwrap();
client
.send_message(
&cid,
data.message_thread_id,
"Reply message",
None,
None,
None,
None,
None,
Some(&r),
)
.await
.unwrap()
.unwrap();
}
Err(_) => {
println!("No chat id specified, skip test.")
}
},
Err(_) => {
println!("No tg bot token specified, skip test.")
}
}
}

View File

@@ -1,2 +1,6 @@
/// Telegram Bot API Client
pub mod botapi_client;
/// Split long messages into multiple messages if needed (Only supports HTML messages) /// Split long messages into multiple messages if needed (Only supports HTML messages)
pub mod text; pub mod text;
/// Telegram params type
pub mod tg_type;

View File

@@ -0,0 +1,183 @@
use derive_builder::Builder;
use derive_more::From;
use serde::{Deserialize, Serialize};
use std::str::FromStr;
#[derive(Clone, Debug, PartialEq, PartialOrd, From, Deserialize, Serialize)]
#[serde(untagged)]
/// Unique identifier for the target chat or username of the target channel
pub enum ChatId {
/// Unique id for chat
Id(i64),
/// Username of the target channel, `@channelusername`
Username(String),
}
impl FromStr for ChatId {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.parse::<i64>() {
Ok(i) => Ok(ChatId::Id(i)),
Err(_) => {
if s.starts_with("@") {
Ok(ChatId::Username(s.to_owned()))
} else {
Err(String::from("Failed to parse chat id."))
}
}
}
}
}
impl ToString for ChatId {
fn to_string(&self) -> String {
match self {
ChatId::Id(i) => format!("{}", i),
ChatId::Username(s) => s.to_owned(),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Deserialize, Serialize)]
/// Formatting options
/// See https://core.telegram.org/bots/api#formatting-options
pub enum ParseMode {
/// https://core.telegram.org/bots/api#markdownv2-style
MarkdownV2,
/// https://core.telegram.org/bots/api#html-style
HTML,
/// https://core.telegram.org/bots/api#markdown-style
Markdown,
}
impl AsRef<str> for ParseMode {
fn as_ref(&self) -> &str {
match self {
ParseMode::HTML => "HTML",
ParseMode::Markdown => "Markdown",
ParseMode::MarkdownV2 => "MarkdownV2",
}
}
}
#[derive(Builder, Clone, Debug, Default, PartialEq, PartialOrd, Deserialize, Serialize)]
#[builder(setter(strip_option))]
#[builder(default)]
/// Describes the options used for link preview generation.
/// See https://core.telegram.org/bots/api#linkpreviewoptions
pub struct LinkPreviewOptions {
/// Optional. True, if the link preview is disabled
#[serde(skip_serializing_if = "Option::is_none")]
is_disabled: Option<bool>,
/// Optional. URL to use for the link preview.
/// If empty, then the first URL found in the message text will be used
#[serde(skip_serializing_if = "Option::is_none")]
url: Option<String>,
/// Optional. True, if the media in the link preview is supposed to be shrunk;
/// ignored if the URL isn't explicitly specified or media size change isn't supported for the preview
#[serde(skip_serializing_if = "Option::is_none")]
prefer_small_media: Option<bool>,
/// Optional. True, if the media in the link preview is supposed to be enlarged;
/// ignored if the URL isn't explicitly specified or media size change isn't supported for the preview
#[serde(skip_serializing_if = "Option::is_none")]
prefer_large_media: Option<bool>,
/// Optional. True, if the link preview must be shown above the message text;
/// otherwise, the link preview will be shown below the message text
#[serde(skip_serializing_if = "Option::is_none")]
show_above_text: Option<bool>,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(untagged)]
pub enum BotApiResult<T>
where
T: Clone + Serialize,
{
Ok {
ok: bool,
result: T,
},
Failed {
ok: bool,
error_code: i64,
description: String,
},
}
impl<T> BotApiResult<T>
where
T: Clone + Serialize,
{
pub fn unwrap(self) -> T {
match self {
Self::Ok { result, .. } => result,
Self::Failed {
description,
error_code,
..
} => panic!("{} ({})", description, error_code),
}
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
/// This object represents a message.
/// https://core.telegram.org/bots/api#message
pub struct Message {
/// Unique message identifier inside this chat
pub message_id: i64,
/// Optional. Unique identifier of a message thread to which the message belongs;
/// for supergroups only
pub message_thread_id: Option<i64>,
}
#[derive(Builder, Clone, Debug, Deserialize, Serialize)]
pub struct ReplyParameters {
/// Identifier of the message that will be replied to in the current chat,
/// or in the chat chat_id if it is specified
message_id: i64,
/// 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)]
#[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
/// to be replied to is not found. Always False for replies in another chat or forum topic.
/// Always True for messages sent on behalf of a business account.
#[builder(setter(strip_option))]
#[builder(default)]
#[serde(skip_serializing_if = "Option::is_none")]
allow_sending_without_reply: Option<bool>,
/// Optional. Quoted part of the message to be replied to; 0-1024 characters after entities parsing.
/// 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)]
#[serde(skip_serializing_if = "Option::is_none")]
quote: Option<String>,
/// Optional. Mode for parsing entities in the quote. See formatting options for more details.
#[builder(default)]
#[serde(skip_serializing_if = "Option::is_none")]
quote_parse_mode: Option<ParseMode>,
/// Optional. Position of the quote in the original message in UTF-16 code units
#[builder(default)]
#[serde(skip_serializing_if = "Option::is_none")]
quote_position: Option<i64>,
}
#[test]
fn test_chat_id() {
assert_eq!(
serde_json::from_str::<ChatId>("32").unwrap(),
ChatId::Id(32)
);
}
#[test]
fn test_parse_mode() {
assert_eq!(
serde_json::from_str::<ParseMode>("\"MarkdownV2\"").unwrap(),
ParseMode::MarkdownV2
);
}