mirror of
https://github.com/lifegpc/pixiv_downloader.git
synced 2026-06-06 05:49:01 +08:00
Add telegram botapi sendMessage
This commit is contained in:
80
Cargo.lock
generated
80
Cargo.lock
generated
@@ -358,6 +358,41 @@ dependencies = [
|
||||
"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]]
|
||||
name = "dateparser"
|
||||
version = "0.2.1"
|
||||
@@ -381,6 +416,37 @@ dependencies = [
|
||||
"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]]
|
||||
name = "derive_more"
|
||||
version = "0.99.18"
|
||||
@@ -867,6 +933,12 @@ dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ident_case"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "0.5.0"
|
||||
@@ -1534,6 +1606,7 @@ dependencies = [
|
||||
"chrono",
|
||||
"cmake",
|
||||
"dateparser",
|
||||
"derive_builder",
|
||||
"derive_more",
|
||||
"fancy-regex",
|
||||
"flagset",
|
||||
@@ -1735,6 +1808,7 @@ dependencies = [
|
||||
"js-sys",
|
||||
"log",
|
||||
"mime",
|
||||
"mime_guess",
|
||||
"native-tls",
|
||||
"once_cell",
|
||||
"percent-encoding",
|
||||
@@ -1998,6 +2072,12 @@ dependencies = [
|
||||
"quote",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "1.0.109"
|
||||
|
||||
@@ -14,6 +14,7 @@ c_fixed_string = { version = "0.2", optional = true }
|
||||
cfg-if = "1"
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
dateparser = "0.2.0"
|
||||
derive_builder = "0.20"
|
||||
derive_more = "0.99"
|
||||
fancy-regex = "0.11"
|
||||
flagset = { version = "0.4", optional = true }
|
||||
@@ -43,11 +44,11 @@ percent-encoding = "*"
|
||||
proc_macros = { path = "proc_macros" }
|
||||
rand = { version = "0", optional = true }
|
||||
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 }
|
||||
RustyXML = "0.3"
|
||||
serde = "1"
|
||||
serde_json = { version = "1", optional = true }
|
||||
serde_json = "1"
|
||||
serde_urlencoded = { version = "*", optional = true }
|
||||
tokio = { version = "1.27", features = ["rt", "macros", "rt-multi-thread", "time"] }
|
||||
url = "2.3"
|
||||
@@ -66,7 +67,7 @@ db_all = ["db", "db_sqlite"]
|
||||
db_sqlite = ["rusqlite"]
|
||||
docker = []
|
||||
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"]
|
||||
|
||||
[patch.crates-io]
|
||||
|
||||
@@ -3,6 +3,5 @@ pub enum SqliteError {
|
||||
DbError(rusqlite::Error),
|
||||
DatabaseVersionTooNew,
|
||||
UserNameAlreadyExists,
|
||||
#[cfg(feature = "serde_json")]
|
||||
SerdeError(serde_json::Error),
|
||||
}
|
||||
|
||||
@@ -29,7 +29,6 @@ pub enum PixivDownloaderError {
|
||||
ParseIntError(std::num::ParseIntError),
|
||||
ReqwestError(reqwest::Error),
|
||||
PixivAppError(crate::pixivapp::error::PixivAppError),
|
||||
#[cfg(feature = "serde_json")]
|
||||
SerdeJsonError(serde_json::Error),
|
||||
#[cfg(feature = "serde_urlencoded")]
|
||||
SerdeUrlencodedError(serde_urlencoded::ser::Error),
|
||||
|
||||
176
src/push/telegram/botapi_client.rs
Normal file
176
src/push/telegram/botapi_client.rs
Normal 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.")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,2 +1,6 @@
|
||||
/// Telegram Bot API Client
|
||||
pub mod botapi_client;
|
||||
/// Split long messages into multiple messages if needed (Only supports HTML messages)
|
||||
pub mod text;
|
||||
/// Telegram params type
|
||||
pub mod tg_type;
|
||||
|
||||
183
src/push/telegram/tg_type.rs
Normal file
183
src/push/telegram/tg_type.rs
Normal 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
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user