mirror of
https://github.com/teloxide/teloxide.git
synced 2025-01-08 19:33:53 +01:00
Merge branch 'master' into add_string_to_api_error_kind_other
This commit is contained in:
commit
66569cafa2
30 changed files with 722 additions and 317 deletions
7
.github/workflows/ci.yml
vendored
7
.github/workflows/ci.yml
vendored
|
@ -1,3 +1,4 @@
|
|||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
|
@ -35,6 +36,12 @@ jobs:
|
|||
profile: minimal
|
||||
toolchain: stable
|
||||
override: true
|
||||
- name: Setup redis
|
||||
run: |
|
||||
sudo apt install redis-server
|
||||
redis-server --port 7777 > /dev/null &
|
||||
redis-server --port 7778 > /dev/null &
|
||||
redis-server --port 7779 > /dev/null &
|
||||
- name: Cargo test
|
||||
run: cargo test --all-features
|
||||
build-example:
|
||||
|
|
|
@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file.
|
|||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [0.3.0] - ???
|
||||
### Changed
|
||||
- Now methods which can send file to Telegram returns tokio::io::Result<T>. Early its could panic. ([issue 216](https://github.com/teloxide/teloxide/issues/216))
|
||||
|
||||
## [0.2.0] - 2020-02-25
|
||||
### Added
|
||||
- The functionality to parse commands only with a correct bot's name (breaks backwards compatibility) ([Issue 168](https://github.com/teloxide/teloxide/issues/168)).
|
||||
|
|
11
Cargo.toml
11
Cargo.toml
|
@ -23,7 +23,10 @@ authors = [
|
|||
[badges]
|
||||
maintenance = { status = "actively-developed" }
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
[features]
|
||||
redis-storage = ["redis"]
|
||||
cbor-serializer = ["serde_cbor"]
|
||||
bincode-serializer = ["bincode"]
|
||||
|
||||
[dependencies]
|
||||
serde_json = "1.0.44"
|
||||
|
@ -45,7 +48,11 @@ futures = "0.3.1"
|
|||
pin-project = "0.4.6"
|
||||
serde_with_macros = "1.0.1"
|
||||
|
||||
teloxide-macros = "0.3.0"
|
||||
redis = { version = "0.15.1", optional = true }
|
||||
serde_cbor = { version = "0.11.1", optional = true }
|
||||
bincode = { version = "1.2.1", optional = true }
|
||||
|
||||
teloxide-macros = "0.3.1"
|
||||
|
||||
[dev-dependencies]
|
||||
smart-default = "0.6.0"
|
||||
|
|
22
README.md
22
README.md
|
@ -35,18 +35,34 @@
|
|||
|
||||
## Features
|
||||
|
||||
<h3 align="center">Type safety</h3>
|
||||
<h3 align="center">Functional reactive design</h3>
|
||||
<p align="center">
|
||||
All the API <a href="https://docs.rs/teloxide/latest/teloxide/types/index.html">types</a> and <a href="https://docs.rs/teloxide/0.2.0/teloxide/requests/index.html">methods</a> are implemented with heavy use of <a href="https://en.wikipedia.org/wiki/Algebraic_data_type"><strong>ADT</strong>s</a> to enforce type safety and tight integration with IDEs. Bot's commands <a href="https://github.com/teloxide/teloxide#commands">have precise types too</a>, thereby serving as a self-documenting code and respecting the <a href="https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-validate/">parse, don't validate</a> programming idiom.
|
||||
teloxide has <a href="https://en.wikipedia.org/wiki/Functional_reactive_programming">functional reactive design</a>, allowing you to declaratively manipulate streams of updates from Telegram using filters, maps, folds, zips, and a lot of <a href="https://docs.rs/futures/latest/futures/stream/trait.StreamExt.html">other adaptors</a>.
|
||||
</p>
|
||||
|
||||
<hr>
|
||||
|
||||
<h3 align="center">API types as ADTs</h3>
|
||||
<p align="center">
|
||||
All the API <a href="https://docs.rs/teloxide/latest/teloxide/types/index.html">types</a> and <a href="https://docs.rs/teloxide/latest/teloxide/requests/index.html">methods</a> are hand-written, with heavy use of <a href="https://en.wikipedia.org/wiki/Algebraic_data_type"><strong>ADT</strong>s</a> (algebraic data types) to enforce type safety and tight integration with IDEs. As few <code>Option</code>s as possible.
|
||||
</p>
|
||||
|
||||
<hr>
|
||||
|
||||
<h3 align="center">Persistence</h3>
|
||||
<p align="center">
|
||||
Dialogues management is independent of how/where they are stored: just replace one line and make them <a href="https://en.wikipedia.org/wiki/Persistence_(computer_science)">persistent</a> (for example, store on a disk, transmit through a network), without affecting the actual <a href="https://en.wikipedia.org/wiki/Finite-state_machine">FSM</a> algorithm. By default, teloxide stores all user dialogues in RAM. Default database implementations <a href="https://github.com/teloxide/teloxide/issues/183">are coming</a>!
|
||||
Dialogues management is independent of how/where dialogues are stored: you can just replace one line and make them <a href="https://en.wikipedia.org/wiki/Persistence_(computer_science)">persistent</a>. Out-of-the-box storages include <a href="https://redis.io/">Redis</a>.
|
||||
</p>
|
||||
|
||||
<hr>
|
||||
|
||||
<h3 align="center">Strongly typed bot commands</h3>
|
||||
<p align="center">
|
||||
You can describe bot commands as enumerations, and then they'll be automatically constructed from strings. Just like you describe JSON structures in <a href="https://github.com/serde-rs/json">serde-json</a> and command-line arguments in <a href="https://github.com/TeXitoi/structopt">structopt</a>.
|
||||
</p>
|
||||
|
||||
<hr>
|
||||
|
||||
## Setting up your environment
|
||||
1. [Download Rust](http://rustup.rs/).
|
||||
2. Create a new bot using [@Botfather](https://t.me/botfather) to get a token in the format `123456789:blablabla`.
|
||||
|
|
|
@ -45,9 +45,9 @@ async fn run() {
|
|||
|
||||
Dispatcher::new(bot)
|
||||
.messages_handler(DialogueDispatcher::new(
|
||||
|cx: DialogueWithCx<Message, Dialogue, Infallible>| async move {
|
||||
|input: TransitionIn<Dialogue, Infallible>| async move {
|
||||
// Unwrap without panic because of std::convert::Infallible.
|
||||
dispatch(cx.cx, cx.dialogue.unwrap())
|
||||
dispatch(input.cx, input.dialogue.unwrap())
|
||||
.await
|
||||
.expect("Something wrong with the bot!")
|
||||
},
|
||||
|
|
16
examples/redis_remember_bot/Cargo.toml
Normal file
16
examples/redis_remember_bot/Cargo.toml
Normal file
|
@ -0,0 +1,16 @@
|
|||
[package]
|
||||
name = "redis_remember_bot"
|
||||
version = "0.1.0"
|
||||
authors = ["Maximilian Siling <mouse-art@ya.ru>"]
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
tokio = "0.2.9"
|
||||
|
||||
# You can also choose "cbor-serializer" or built-in JSON serializer
|
||||
teloxide = { path = "../../", features = ["redis-storage", "bincode-serializer"] }
|
||||
serde = "1.0.104"
|
||||
|
||||
thiserror = "1.0.15"
|
||||
smart-default = "0.6.0"
|
||||
derive_more = "0.99.9"
|
66
examples/redis_remember_bot/src/main.rs
Normal file
66
examples/redis_remember_bot/src/main.rs
Normal file
|
@ -0,0 +1,66 @@
|
|||
#[macro_use]
|
||||
extern crate smart_default;
|
||||
#[macro_use]
|
||||
extern crate derive_more;
|
||||
|
||||
mod states;
|
||||
mod transitions;
|
||||
|
||||
use states::*;
|
||||
use transitions::*;
|
||||
|
||||
use teloxide::{
|
||||
dispatching::dialogue::{serializer::Bincode, RedisStorage, Storage},
|
||||
prelude::*,
|
||||
};
|
||||
use thiserror::Error;
|
||||
|
||||
type StorageError = <RedisStorage<Bincode> as Storage<Dialogue>>::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
enum Error {
|
||||
#[error("error from Telegram: {0}")]
|
||||
TelegramError(#[from] RequestError),
|
||||
#[error("error from storage: {0}")]
|
||||
StorageError(#[from] StorageError),
|
||||
}
|
||||
|
||||
type In = TransitionIn<Dialogue, StorageError>;
|
||||
|
||||
async fn handle_message(input: In) -> Out {
|
||||
let (cx, dialogue) = input.unpack();
|
||||
|
||||
match cx.update.text_owned() {
|
||||
Some(text) => dispatch(cx, dialogue, &text).await,
|
||||
None => {
|
||||
cx.answer_str("Please, send me a text message").await?;
|
||||
next(StartState)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
run().await;
|
||||
}
|
||||
|
||||
async fn run() {
|
||||
let bot = Bot::from_env();
|
||||
Dispatcher::new(bot)
|
||||
.messages_handler(DialogueDispatcher::with_storage(
|
||||
|cx| async move {
|
||||
handle_message(cx)
|
||||
.await
|
||||
.expect("Something is wrong with the bot!")
|
||||
},
|
||||
// You can also choose serializer::JSON or serializer::CBOR
|
||||
// All serializers but JSON require enabling feature
|
||||
// "serializer-<name>", e. g. "serializer-cbor"
|
||||
// or "serializer-bincode"
|
||||
RedisStorage::open("redis://127.0.0.1:6379", Bincode)
|
||||
.await
|
||||
.unwrap(),
|
||||
))
|
||||
.dispatch()
|
||||
.await;
|
||||
}
|
23
examples/redis_remember_bot/src/states.rs
Normal file
23
examples/redis_remember_bot/src/states.rs
Normal file
|
@ -0,0 +1,23 @@
|
|||
use teloxide::prelude::*;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Default, Serialize, Deserialize)]
|
||||
pub struct StartState;
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct HaveNumberState {
|
||||
rest: StartState,
|
||||
pub number: i32,
|
||||
}
|
||||
|
||||
up!(
|
||||
StartState + [number: i32] -> HaveNumberState,
|
||||
);
|
||||
|
||||
#[derive(SmartDefault, From, Serialize, Deserialize)]
|
||||
pub enum Dialogue {
|
||||
#[default]
|
||||
Start(StartState),
|
||||
HaveNumber(HaveNumberState),
|
||||
}
|
42
examples/redis_remember_bot/src/transitions.rs
Normal file
42
examples/redis_remember_bot/src/transitions.rs
Normal file
|
@ -0,0 +1,42 @@
|
|||
use teloxide::prelude::*;
|
||||
|
||||
use super::states::*;
|
||||
|
||||
pub type Cx = UpdateWithCx<Message>;
|
||||
pub type Out = TransitionOut<Dialogue>;
|
||||
|
||||
async fn start(cx: Cx, state: StartState, text: &str) -> Out {
|
||||
if let Ok(number) = text.parse() {
|
||||
cx.answer_str(format!(
|
||||
"Remembered number {}. Now use /get or /reset",
|
||||
number
|
||||
))
|
||||
.await?;
|
||||
next(state.up(number))
|
||||
} else {
|
||||
cx.answer_str("Please, send me a number").await?;
|
||||
next(state)
|
||||
}
|
||||
}
|
||||
|
||||
async fn have_number(cx: Cx, state: HaveNumberState, text: &str) -> Out {
|
||||
let num = state.number;
|
||||
|
||||
if text.starts_with("/get") {
|
||||
cx.answer_str(format!("Here is your number: {}", num)).await?;
|
||||
next(state)
|
||||
} else if text.starts_with("/reset") {
|
||||
cx.answer_str("Resetted number").await?;
|
||||
next(StartState)
|
||||
} else {
|
||||
cx.answer_str("Please, send /get or /reset").await?;
|
||||
next(state)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn dispatch(cx: Cx, dialogue: Dialogue, text: &str) -> Out {
|
||||
match dialogue {
|
||||
Dialogue::Start(state) => start(cx, state, text).await,
|
||||
Dialogue::HaveNumber(state) => have_number(cx, state, text).await,
|
||||
}
|
||||
}
|
|
@ -55,7 +55,11 @@ pub use dialogue_dispatcher_handler::DialogueDispatcherHandler;
|
|||
pub use dialogue_stage::{exit, next, DialogueStage};
|
||||
pub use dialogue_with_cx::DialogueWithCx;
|
||||
pub use get_chat_id::GetChatId;
|
||||
pub use storage::{InMemStorage, Storage};
|
||||
|
||||
#[cfg(feature = "redis-storage")]
|
||||
pub use storage::{RedisStorage, RedisStorageError};
|
||||
|
||||
pub use storage::{serializer, InMemStorage, Serializer, Storage};
|
||||
|
||||
/// Generates `.up(field)` methods for dialogue states.
|
||||
///
|
||||
|
|
|
@ -1,7 +1,16 @@
|
|||
pub mod serializer;
|
||||
|
||||
mod in_mem_storage;
|
||||
|
||||
#[cfg(feature = "redis-storage")]
|
||||
mod redis_storage;
|
||||
|
||||
use futures::future::BoxFuture;
|
||||
|
||||
pub use in_mem_storage::InMemStorage;
|
||||
#[cfg(feature = "redis-storage")]
|
||||
pub use redis_storage::{RedisStorage, RedisStorageError};
|
||||
pub use serializer::Serializer;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// A storage of dialogues.
|
||||
|
|
112
src/dispatching/dialogue/storage/redis_storage.rs
Normal file
112
src/dispatching/dialogue/storage/redis_storage.rs
Normal file
|
@ -0,0 +1,112 @@
|
|||
use super::{serializer::Serializer, Storage};
|
||||
use futures::future::BoxFuture;
|
||||
use redis::{AsyncCommands, FromRedisValue, IntoConnectionInfo};
|
||||
use serde::{de::DeserializeOwned, Serialize};
|
||||
use std::{
|
||||
convert::Infallible,
|
||||
fmt::{Debug, Display},
|
||||
ops::DerefMut,
|
||||
sync::Arc,
|
||||
};
|
||||
use thiserror::Error;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
/// An error returned from [`RedisStorage`].
|
||||
///
|
||||
/// [`RedisStorage`]: struct.RedisStorage.html
|
||||
#[derive(Debug, Error)]
|
||||
pub enum RedisStorageError<SE>
|
||||
where
|
||||
SE: Debug + Display,
|
||||
{
|
||||
#[error("parsing/serializing error: {0}")]
|
||||
SerdeError(SE),
|
||||
#[error("error from Redis: {0}")]
|
||||
RedisError(#[from] redis::RedisError),
|
||||
}
|
||||
|
||||
/// A memory storage based on [Redis](https://redis.io/).
|
||||
pub struct RedisStorage<S> {
|
||||
conn: Mutex<redis::aio::Connection>,
|
||||
serializer: S,
|
||||
}
|
||||
|
||||
impl<S> RedisStorage<S> {
|
||||
pub async fn open(
|
||||
url: impl IntoConnectionInfo,
|
||||
serializer: S,
|
||||
) -> Result<Arc<Self>, RedisStorageError<Infallible>> {
|
||||
Ok(Arc::new(Self {
|
||||
conn: Mutex::new(
|
||||
redis::Client::open(url)?.get_async_connection().await?,
|
||||
),
|
||||
serializer,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, D> Storage<D> for RedisStorage<S>
|
||||
where
|
||||
S: Send + Sync + Serializer<D> + 'static,
|
||||
D: Send + Serialize + DeserializeOwned + 'static,
|
||||
<S as Serializer<D>>::Error: Debug + Display,
|
||||
{
|
||||
type Error = RedisStorageError<<S as Serializer<D>>::Error>;
|
||||
|
||||
// `.del().ignore()` is much more readable than `.del()\n.ignore()`
|
||||
#[rustfmt::skip]
|
||||
fn remove_dialogue(
|
||||
self: Arc<Self>,
|
||||
chat_id: i64,
|
||||
) -> BoxFuture<'static, Result<Option<D>, Self::Error>> {
|
||||
Box::pin(async move {
|
||||
let res = redis::pipe()
|
||||
.atomic()
|
||||
.get(chat_id)
|
||||
.del(chat_id).ignore()
|
||||
.query_async::<_, redis::Value>(
|
||||
self.conn.lock().await.deref_mut(),
|
||||
)
|
||||
.await?;
|
||||
// We're expecting `.pipe()` to return us an exactly one result in
|
||||
// bulk, so all other branches should be unreachable
|
||||
match res {
|
||||
redis::Value::Bulk(bulk) if bulk.len() == 1 => {
|
||||
Ok(Option::<Vec<u8>>::from_redis_value(&bulk[0])?
|
||||
.map(|v| {
|
||||
self.serializer
|
||||
.deserialize(&v)
|
||||
.map_err(RedisStorageError::SerdeError)
|
||||
})
|
||||
.transpose()?)
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn update_dialogue(
|
||||
self: Arc<Self>,
|
||||
chat_id: i64,
|
||||
dialogue: D,
|
||||
) -> BoxFuture<'static, Result<Option<D>, Self::Error>> {
|
||||
Box::pin(async move {
|
||||
let dialogue = self
|
||||
.serializer
|
||||
.serialize(&dialogue)
|
||||
.map_err(RedisStorageError::SerdeError)?;
|
||||
Ok(self
|
||||
.conn
|
||||
.lock()
|
||||
.await
|
||||
.getset::<_, Vec<u8>, Option<Vec<u8>>>(chat_id, dialogue)
|
||||
.await?
|
||||
.map(|d| {
|
||||
self.serializer
|
||||
.deserialize(&d)
|
||||
.map_err(RedisStorageError::SerdeError)
|
||||
})
|
||||
.transpose()?)
|
||||
})
|
||||
}
|
||||
}
|
68
src/dispatching/dialogue/storage/serializer.rs
Normal file
68
src/dispatching/dialogue/storage/serializer.rs
Normal file
|
@ -0,0 +1,68 @@
|
|||
/// Various serializers for memory storages.
|
||||
use serde::{de::DeserializeOwned, ser::Serialize};
|
||||
|
||||
/// A serializer for memory storages.
|
||||
pub trait Serializer<D> {
|
||||
type Error;
|
||||
|
||||
fn serialize(&self, val: &D) -> Result<Vec<u8>, Self::Error>;
|
||||
fn deserialize(&self, data: &[u8]) -> Result<D, Self::Error>;
|
||||
}
|
||||
|
||||
/// The JSON serializer for memory storages.
|
||||
pub struct JSON;
|
||||
|
||||
impl<D> Serializer<D> for JSON
|
||||
where
|
||||
D: Serialize + DeserializeOwned,
|
||||
{
|
||||
type Error = serde_json::Error;
|
||||
|
||||
fn serialize(&self, val: &D) -> Result<Vec<u8>, Self::Error> {
|
||||
serde_json::to_vec(val)
|
||||
}
|
||||
|
||||
fn deserialize(&self, data: &[u8]) -> Result<D, Self::Error> {
|
||||
serde_json::from_slice(data)
|
||||
}
|
||||
}
|
||||
|
||||
/// The CBOR serializer for memory storages.
|
||||
#[cfg(feature = "cbor-serializer")]
|
||||
pub struct CBOR;
|
||||
|
||||
#[cfg(feature = "cbor-serializer")]
|
||||
impl<D> Serializer<D> for CBOR
|
||||
where
|
||||
D: Serialize + DeserializeOwned,
|
||||
{
|
||||
type Error = serde_cbor::Error;
|
||||
|
||||
fn serialize(&self, val: &D) -> Result<Vec<u8>, Self::Error> {
|
||||
serde_cbor::to_vec(val)
|
||||
}
|
||||
|
||||
fn deserialize(&self, data: &[u8]) -> Result<D, Self::Error> {
|
||||
serde_cbor::from_slice(data)
|
||||
}
|
||||
}
|
||||
|
||||
/// The Bincode serializer for memory storages.
|
||||
#[cfg(feature = "bincode-serializer")]
|
||||
pub struct Bincode;
|
||||
|
||||
#[cfg(feature = "bincode-serializer")]
|
||||
impl<D> Serializer<D> for Bincode
|
||||
where
|
||||
D: Serialize + DeserializeOwned,
|
||||
{
|
||||
type Error = bincode::Error;
|
||||
|
||||
fn serialize(&self, val: &D) -> Result<Vec<u8>, Self::Error> {
|
||||
bincode::serialize(val)
|
||||
}
|
||||
|
||||
fn deserialize(&self, data: &[u8]) -> Result<D, Self::Error> {
|
||||
bincode::deserialize(data)
|
||||
}
|
||||
}
|
|
@ -5,7 +5,7 @@ use crate::{
|
|||
Bot,
|
||||
};
|
||||
|
||||
use crate::requests::{Request, ResponseResult};
|
||||
use crate::requests::{RequestWithFile, ResponseResult};
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Use this method to add a new sticker to a set created by the bot.
|
||||
|
@ -22,28 +22,24 @@ pub struct AddStickerToSet {
|
|||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Request for AddStickerToSet {
|
||||
impl RequestWithFile for AddStickerToSet {
|
||||
type Output = True;
|
||||
|
||||
async fn send(&self) -> ResponseResult<True> {
|
||||
net::request_multipart(
|
||||
async fn send(&self) -> tokio::io::Result<ResponseResult<True>> {
|
||||
Ok(net::request_multipart(
|
||||
self.bot.client(),
|
||||
self.bot.token(),
|
||||
"addStickerToSet",
|
||||
FormBuilder::new()
|
||||
.add("user_id", &self.user_id)
|
||||
.await
|
||||
.add("name", &self.name)
|
||||
.await
|
||||
.add("png_sticker", &self.png_sticker)
|
||||
.await
|
||||
.add("emojis", &self.emojis)
|
||||
.await
|
||||
.add("mask_position", &self.mask_position)
|
||||
.await
|
||||
.add_text("user_id", &self.user_id)
|
||||
.add_text("name", &self.name)
|
||||
.add_input_file("png_sticker", &self.png_sticker)
|
||||
.await?
|
||||
.add_text("emojis", &self.emojis)
|
||||
.add_text("mask_position", &self.mask_position)
|
||||
.build(),
|
||||
)
|
||||
.await
|
||||
.await)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use crate::{
|
||||
net,
|
||||
requests::{form_builder::FormBuilder, Request, ResponseResult},
|
||||
requests::{form_builder::FormBuilder, RequestWithFile, ResponseResult},
|
||||
types::{InputFile, MaskPosition, True},
|
||||
Bot,
|
||||
};
|
||||
|
@ -23,32 +23,26 @@ pub struct CreateNewStickerSet {
|
|||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Request for CreateNewStickerSet {
|
||||
impl RequestWithFile for CreateNewStickerSet {
|
||||
type Output = True;
|
||||
|
||||
async fn send(&self) -> ResponseResult<True> {
|
||||
net::request_multipart(
|
||||
async fn send(&self) -> tokio::io::Result<ResponseResult<True>> {
|
||||
Ok(net::request_multipart(
|
||||
self.bot.client(),
|
||||
self.bot.token(),
|
||||
"createNewStickerSet",
|
||||
FormBuilder::new()
|
||||
.add("user_id", &self.user_id)
|
||||
.await
|
||||
.add("name", &self.name)
|
||||
.await
|
||||
.add("title", &self.title)
|
||||
.await
|
||||
.add("png_sticker", &self.png_sticker)
|
||||
.await
|
||||
.add("emojis", &self.emojis)
|
||||
.await
|
||||
.add("contains_masks", &self.contains_masks)
|
||||
.await
|
||||
.add("mask_position", &self.mask_position)
|
||||
.await
|
||||
.add_text("user_id", &self.user_id)
|
||||
.add_text("name", &self.name)
|
||||
.add_text("title", &self.title)
|
||||
.add_input_file("png_sticker", &self.png_sticker)
|
||||
.await?
|
||||
.add_text("emojis", &self.emojis)
|
||||
.add_text("contains_masks", &self.contains_masks)
|
||||
.add_text("mask_position", &self.mask_position)
|
||||
.build(),
|
||||
)
|
||||
.await
|
||||
.await)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -38,14 +38,12 @@ impl Request for EditMessageMedia {
|
|||
match &self.chat_or_inline_message {
|
||||
ChatOrInlineMessage::Chat { chat_id, message_id } => {
|
||||
params = params
|
||||
.add("chat_id", chat_id)
|
||||
.await
|
||||
.add("message_id", message_id)
|
||||
.await;
|
||||
.add_text("chat_id", chat_id)
|
||||
.add_text("message_id", message_id);
|
||||
}
|
||||
ChatOrInlineMessage::Inline { inline_message_id } => {
|
||||
params =
|
||||
params.add("inline_message_id", inline_message_id).await;
|
||||
params.add_text("inline_message_id", inline_message_id);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -54,10 +52,8 @@ impl Request for EditMessageMedia {
|
|||
self.bot.token(),
|
||||
"editMessageMedia",
|
||||
params
|
||||
.add("media", &self.media)
|
||||
.await
|
||||
.add("reply_markup", &self.reply_markup)
|
||||
.await
|
||||
.add_text("media", &self.media)
|
||||
.add_text("reply_markup", &self.reply_markup)
|
||||
.build(),
|
||||
)
|
||||
.await
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use crate::{
|
||||
net,
|
||||
requests::{form_builder::FormBuilder, Request, ResponseResult},
|
||||
requests::{form_builder::FormBuilder, RequestWithFile, ResponseResult},
|
||||
types::{ChatId, InputFile, Message, ParseMode, ReplyMarkup},
|
||||
Bot,
|
||||
};
|
||||
|
@ -30,40 +30,32 @@ pub struct SendAnimation {
|
|||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Request for SendAnimation {
|
||||
impl RequestWithFile for SendAnimation {
|
||||
type Output = Message;
|
||||
|
||||
async fn send(&self) -> ResponseResult<Message> {
|
||||
net::request_multipart(
|
||||
async fn send(&self) -> tokio::io::Result<ResponseResult<Message>> {
|
||||
let mut builder = FormBuilder::new()
|
||||
.add_text("chat_id", &self.chat_id)
|
||||
.add_input_file("animation", &self.animation)
|
||||
.await?
|
||||
.add_text("duration", &self.duration)
|
||||
.add_text("width", &self.width)
|
||||
.add_text("height", &self.height)
|
||||
.add_text("caption", &self.caption)
|
||||
.add_text("parse_mode", &self.parse_mode)
|
||||
.add_text("disable_notification", &self.disable_notification)
|
||||
.add_text("reply_to_message_id", &self.reply_to_message_id)
|
||||
.add_text("reply_markup", &self.reply_markup);
|
||||
if let Some(thumb) = self.thumb.as_ref() {
|
||||
builder = builder.add_input_file("thumb", thumb).await?;
|
||||
}
|
||||
Ok(net::request_multipart(
|
||||
self.bot.client(),
|
||||
self.bot.token(),
|
||||
"sendAnimation",
|
||||
FormBuilder::new()
|
||||
.add("chat_id", &self.chat_id)
|
||||
.await
|
||||
.add("animation", &self.animation)
|
||||
.await
|
||||
.add("duration", &self.duration)
|
||||
.await
|
||||
.add("width", &self.width)
|
||||
.await
|
||||
.add("height", &self.height)
|
||||
.await
|
||||
.add("thumb", &self.thumb)
|
||||
.await
|
||||
.add("caption", &self.caption)
|
||||
.await
|
||||
.add("parse_mode", &self.parse_mode)
|
||||
.await
|
||||
.add("disable_notification", &self.disable_notification)
|
||||
.await
|
||||
.add("reply_to_message_id", &self.reply_to_message_id)
|
||||
.await
|
||||
.add("reply_markup", &self.reply_markup)
|
||||
.await
|
||||
.build(),
|
||||
builder.build(),
|
||||
)
|
||||
.await
|
||||
.await)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use crate::{
|
||||
net,
|
||||
requests::{form_builder::FormBuilder, Request, ResponseResult},
|
||||
requests::{form_builder::FormBuilder, RequestWithFile, ResponseResult},
|
||||
types::{ChatId, InputFile, Message, ParseMode, ReplyMarkup},
|
||||
Bot,
|
||||
};
|
||||
|
@ -34,40 +34,32 @@ pub struct SendAudio {
|
|||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Request for SendAudio {
|
||||
impl RequestWithFile for SendAudio {
|
||||
type Output = Message;
|
||||
|
||||
async fn send(&self) -> ResponseResult<Message> {
|
||||
net::request_multipart(
|
||||
async fn send(&self) -> tokio::io::Result<ResponseResult<Message>> {
|
||||
let mut builder = FormBuilder::new()
|
||||
.add_text("chat_id", &self.chat_id)
|
||||
.add_input_file("audio", &self.audio)
|
||||
.await?
|
||||
.add_text("caption", &self.caption)
|
||||
.add_text("parse_mode", &self.parse_mode)
|
||||
.add_text("duration", &self.duration)
|
||||
.add_text("performer", &self.performer)
|
||||
.add_text("title", &self.title)
|
||||
.add_text("disable_notification", &self.disable_notification)
|
||||
.add_text("reply_to_message_id", &self.reply_to_message_id)
|
||||
.add_text("reply_markup", &self.reply_markup);
|
||||
if let Some(thumb) = self.thumb.as_ref() {
|
||||
builder = builder.add_input_file("thumb", thumb).await?;
|
||||
}
|
||||
Ok(net::request_multipart(
|
||||
self.bot.client(),
|
||||
self.bot.token(),
|
||||
"sendAudio",
|
||||
FormBuilder::new()
|
||||
.add("chat_id", &self.chat_id)
|
||||
.await
|
||||
.add("audio", &self.audio)
|
||||
.await
|
||||
.add("caption", &self.caption)
|
||||
.await
|
||||
.add("parse_mode", &self.parse_mode)
|
||||
.await
|
||||
.add("duration", &self.duration)
|
||||
.await
|
||||
.add("performer", &self.performer)
|
||||
.await
|
||||
.add("title", &self.title)
|
||||
.await
|
||||
.add("thumb", &self.thumb)
|
||||
.await
|
||||
.add("disable_notification", &self.disable_notification)
|
||||
.await
|
||||
.add("reply_to_message_id", &self.reply_to_message_id)
|
||||
.await
|
||||
.add("reply_markup", &self.reply_markup)
|
||||
.await
|
||||
.build(),
|
||||
builder.build(),
|
||||
)
|
||||
.await
|
||||
.await)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use crate::{
|
||||
net,
|
||||
requests::{form_builder::FormBuilder, Request, ResponseResult},
|
||||
requests::{form_builder::FormBuilder, RequestWithFile, ResponseResult},
|
||||
types::{ChatId, InputFile, Message, ParseMode, ReplyMarkup},
|
||||
Bot,
|
||||
};
|
||||
|
@ -26,34 +26,29 @@ pub struct SendDocument {
|
|||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Request for SendDocument {
|
||||
impl RequestWithFile for SendDocument {
|
||||
type Output = Message;
|
||||
|
||||
async fn send(&self) -> ResponseResult<Message> {
|
||||
net::request_multipart(
|
||||
async fn send(&self) -> tokio::io::Result<ResponseResult<Message>> {
|
||||
let mut builder = FormBuilder::new()
|
||||
.add_text("chat_id", &self.chat_id)
|
||||
.add_input_file("document", &self.document)
|
||||
.await?
|
||||
.add_text("caption", &self.caption)
|
||||
.add_text("parse_mode", &self.parse_mode)
|
||||
.add_text("disable_notification", &self.disable_notification)
|
||||
.add_text("reply_to_message_id", &self.reply_to_message_id)
|
||||
.add_text("reply_markup", &self.reply_markup);
|
||||
if let Some(thumb) = self.thumb.as_ref() {
|
||||
builder = builder.add_input_file("thumb", thumb).await?;
|
||||
}
|
||||
Ok(net::request_multipart(
|
||||
self.bot.client(),
|
||||
self.bot.token(),
|
||||
"sendDocument",
|
||||
FormBuilder::new()
|
||||
.add("chat_id", &self.chat_id)
|
||||
.await
|
||||
.add("document", &self.document)
|
||||
.await
|
||||
.add("thumb", &self.thumb)
|
||||
.await
|
||||
.add("caption", &self.caption)
|
||||
.await
|
||||
.add("parse_mode", &self.parse_mode)
|
||||
.await
|
||||
.add("disable_notification", &self.disable_notification)
|
||||
.await
|
||||
.add("reply_to_message_id", &self.reply_to_message_id)
|
||||
.await
|
||||
.add("reply_markup", &self.reply_markup)
|
||||
.await
|
||||
.build(),
|
||||
builder.build(),
|
||||
)
|
||||
.await
|
||||
.await)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -28,14 +28,10 @@ impl Request for SendMediaGroup {
|
|||
self.bot.token(),
|
||||
"sendMediaGroup",
|
||||
FormBuilder::new()
|
||||
.add("chat_id", &self.chat_id)
|
||||
.await
|
||||
.add("media", &self.media)
|
||||
.await
|
||||
.add("disable_notification", &self.disable_notification)
|
||||
.await
|
||||
.add("reply_to_message_id", &self.reply_to_message_id)
|
||||
.await
|
||||
.add_text("chat_id", &self.chat_id)
|
||||
.add_text("media", &self.media)
|
||||
.add_text("disable_notification", &self.disable_notification)
|
||||
.add_text("reply_to_message_id", &self.reply_to_message_id)
|
||||
.build(),
|
||||
)
|
||||
.await
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use crate::{
|
||||
net,
|
||||
requests::{form_builder::FormBuilder, Request, ResponseResult},
|
||||
requests::{form_builder::FormBuilder, RequestWithFile, ResponseResult},
|
||||
types::{ChatId, InputFile, Message, ParseMode, ReplyMarkup},
|
||||
Bot,
|
||||
};
|
||||
|
@ -22,32 +22,26 @@ pub struct SendPhoto {
|
|||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Request for SendPhoto {
|
||||
impl RequestWithFile for SendPhoto {
|
||||
type Output = Message;
|
||||
|
||||
async fn send(&self) -> ResponseResult<Message> {
|
||||
net::request_multipart(
|
||||
async fn send(&self) -> tokio::io::Result<ResponseResult<Message>> {
|
||||
Ok(net::request_multipart(
|
||||
self.bot.client(),
|
||||
self.bot.token(),
|
||||
"sendPhoto",
|
||||
FormBuilder::new()
|
||||
.add("chat_id", &self.chat_id)
|
||||
.await
|
||||
.add("photo", &self.photo)
|
||||
.await
|
||||
.add("caption", &self.caption)
|
||||
.await
|
||||
.add("parse_mode", &self.parse_mode)
|
||||
.await
|
||||
.add("disable_notification", &self.disable_notification)
|
||||
.await
|
||||
.add("reply_to_message_id", &self.reply_to_message_id)
|
||||
.await
|
||||
.add("reply_markup", &self.reply_markup)
|
||||
.await
|
||||
.add_text("chat_id", &self.chat_id)
|
||||
.add_input_file("photo", &self.photo)
|
||||
.await?
|
||||
.add_text("caption", &self.caption)
|
||||
.add_text("parse_mode", &self.parse_mode)
|
||||
.add_text("disable_notification", &self.disable_notification)
|
||||
.add_text("reply_to_message_id", &self.reply_to_message_id)
|
||||
.add_text("reply_markup", &self.reply_markup)
|
||||
.build(),
|
||||
)
|
||||
.await
|
||||
.await)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use crate::{
|
||||
net,
|
||||
requests::{form_builder::FormBuilder, Request, ResponseResult},
|
||||
requests::{form_builder::FormBuilder, RequestWithFile, ResponseResult},
|
||||
types::{ChatId, InputFile, Message, ReplyMarkup},
|
||||
Bot,
|
||||
};
|
||||
|
@ -22,28 +22,24 @@ pub struct SendSticker {
|
|||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Request for SendSticker {
|
||||
impl RequestWithFile for SendSticker {
|
||||
type Output = Message;
|
||||
|
||||
async fn send(&self) -> ResponseResult<Message> {
|
||||
net::request_multipart(
|
||||
async fn send(&self) -> tokio::io::Result<ResponseResult<Message>> {
|
||||
Ok(net::request_multipart(
|
||||
self.bot.client(),
|
||||
self.bot.token(),
|
||||
"sendSticker",
|
||||
FormBuilder::new()
|
||||
.add("chat_id", &self.chat_id)
|
||||
.await
|
||||
.add("sticker", &self.sticker)
|
||||
.await
|
||||
.add("disable_notification", &self.disable_notification)
|
||||
.await
|
||||
.add("reply_to_message_id", &self.reply_to_message_id)
|
||||
.await
|
||||
.add("reply_markup", &self.reply_markup)
|
||||
.await
|
||||
.add_text("chat_id", &self.chat_id)
|
||||
.add_input_file("sticker", &self.sticker)
|
||||
.await?
|
||||
.add_text("disable_notification", &self.disable_notification)
|
||||
.add_text("reply_to_message_id", &self.reply_to_message_id)
|
||||
.add_text("reply_markup", &self.reply_markup)
|
||||
.build(),
|
||||
)
|
||||
.await
|
||||
.await)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use crate::{
|
||||
net,
|
||||
requests::{form_builder::FormBuilder, Request, ResponseResult},
|
||||
requests::{form_builder::FormBuilder, RequestWithFile, ResponseResult},
|
||||
types::{ChatId, InputFile, Message, ParseMode, ReplyMarkup},
|
||||
Bot,
|
||||
};
|
||||
|
@ -31,42 +31,33 @@ pub struct SendVideo {
|
|||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Request for SendVideo {
|
||||
impl RequestWithFile for SendVideo {
|
||||
type Output = Message;
|
||||
|
||||
async fn send(&self) -> ResponseResult<Message> {
|
||||
net::request_multipart(
|
||||
async fn send(&self) -> tokio::io::Result<ResponseResult<Message>> {
|
||||
let mut builder = FormBuilder::new()
|
||||
.add_text("chat_id", &self.chat_id)
|
||||
.add_input_file("video", &self.video)
|
||||
.await?
|
||||
.add_text("duration", &self.duration)
|
||||
.add_text("width", &self.width)
|
||||
.add_text("height", &self.height)
|
||||
.add_text("caption", &self.caption)
|
||||
.add_text("parse_mode", &self.parse_mode)
|
||||
.add_text("supports_streaming", &self.supports_streaming)
|
||||
.add_text("disable_notification", &self.disable_notification)
|
||||
.add_text("reply_to_message_id", &self.reply_to_message_id)
|
||||
.add_text("reply_markup", &self.reply_markup);
|
||||
if let Some(thumb) = self.thumb.as_ref() {
|
||||
builder = builder.add_input_file("thumb", thumb).await?;
|
||||
}
|
||||
Ok(net::request_multipart(
|
||||
self.bot.client(),
|
||||
self.bot.token(),
|
||||
"sendVideo",
|
||||
FormBuilder::new()
|
||||
.add("chat_id", &self.chat_id)
|
||||
.await
|
||||
.add("video", &self.video)
|
||||
.await
|
||||
.add("duration", &self.duration)
|
||||
.await
|
||||
.add("width", &self.width)
|
||||
.await
|
||||
.add("height", &self.height)
|
||||
.await
|
||||
.add("thumb", &self.thumb)
|
||||
.await
|
||||
.add("caption", &self.caption)
|
||||
.await
|
||||
.add("parse_mode", &self.parse_mode)
|
||||
.await
|
||||
.add("supports_streaming", &self.supports_streaming)
|
||||
.await
|
||||
.add("disable_notification", &self.disable_notification)
|
||||
.await
|
||||
.add("reply_to_message_id", &self.reply_to_message_id)
|
||||
.await
|
||||
.add("reply_markup", &self.reply_markup)
|
||||
.await
|
||||
.build(),
|
||||
builder.build(),
|
||||
)
|
||||
.await
|
||||
.await)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use crate::{
|
||||
net,
|
||||
requests::{form_builder::FormBuilder, Request, ResponseResult},
|
||||
requests::{form_builder::FormBuilder, RequestWithFile, ResponseResult},
|
||||
types::{ChatId, InputFile, Message, ReplyMarkup},
|
||||
Bot,
|
||||
};
|
||||
|
@ -26,34 +26,29 @@ pub struct SendVideoNote {
|
|||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Request for SendVideoNote {
|
||||
impl RequestWithFile for SendVideoNote {
|
||||
type Output = Message;
|
||||
|
||||
async fn send(&self) -> ResponseResult<Message> {
|
||||
net::request_multipart(
|
||||
async fn send(&self) -> tokio::io::Result<ResponseResult<Message>> {
|
||||
let mut builder = FormBuilder::new()
|
||||
.add_text("chat_id", &self.chat_id)
|
||||
.add_input_file("video_note", &self.video_note)
|
||||
.await?
|
||||
.add_text("duration", &self.duration)
|
||||
.add_text("length", &self.length)
|
||||
.add_text("disable_notification", &self.disable_notification)
|
||||
.add_text("reply_to_message_id", &self.reply_to_message_id)
|
||||
.add_text("reply_markup", &self.reply_markup);
|
||||
if let Some(thumb) = self.thumb.as_ref() {
|
||||
builder = builder.add_input_file("thumb", thumb).await?;
|
||||
}
|
||||
Ok(net::request_multipart(
|
||||
self.bot.client(),
|
||||
self.bot.token(),
|
||||
"sendVideoNote",
|
||||
FormBuilder::new()
|
||||
.add("chat_id", &self.chat_id)
|
||||
.await
|
||||
.add("video_note", &self.video_note)
|
||||
.await
|
||||
.add("duration", &self.duration)
|
||||
.await
|
||||
.add("length", &self.length)
|
||||
.await
|
||||
.add("thumb", &self.thumb)
|
||||
.await
|
||||
.add("disable_notification", &self.disable_notification)
|
||||
.await
|
||||
.add("reply_to_message_id", &self.reply_to_message_id)
|
||||
.await
|
||||
.add("reply_markup", &self.reply_markup)
|
||||
.await
|
||||
.build(),
|
||||
builder.build(),
|
||||
)
|
||||
.await
|
||||
.await)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use crate::{
|
||||
net,
|
||||
requests::{form_builder::FormBuilder, Request, ResponseResult},
|
||||
requests::{form_builder::FormBuilder, RequestWithFile, ResponseResult},
|
||||
types::{ChatId, InputFile, Message, ParseMode, ReplyMarkup},
|
||||
Bot,
|
||||
};
|
||||
|
@ -32,34 +32,27 @@ pub struct SendVoice {
|
|||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Request for SendVoice {
|
||||
impl RequestWithFile for SendVoice {
|
||||
type Output = Message;
|
||||
|
||||
async fn send(&self) -> ResponseResult<Message> {
|
||||
net::request_multipart(
|
||||
async fn send(&self) -> tokio::io::Result<ResponseResult<Message>> {
|
||||
Ok(net::request_multipart(
|
||||
self.bot.client(),
|
||||
self.bot.token(),
|
||||
"sendVoice",
|
||||
FormBuilder::new()
|
||||
.add("chat_id", &self.chat_id)
|
||||
.await
|
||||
.add("voice", &self.voice)
|
||||
.await
|
||||
.add("caption", &self.caption)
|
||||
.await
|
||||
.add("parse_mode", &self.parse_mode)
|
||||
.await
|
||||
.add("duration", &self.duration)
|
||||
.await
|
||||
.add("disable_notification", &self.disable_notification)
|
||||
.await
|
||||
.add("reply_to_message_id", &self.reply_to_message_id)
|
||||
.await
|
||||
.add("reply_markup", &self.reply_markup)
|
||||
.await
|
||||
.add_text("chat_id", &self.chat_id)
|
||||
.add_input_file("voice", &self.voice)
|
||||
.await?
|
||||
.add_text("caption", &self.caption)
|
||||
.add_text("parse_mode", &self.parse_mode)
|
||||
.add_text("duration", &self.duration)
|
||||
.add_text("disable_notification", &self.disable_notification)
|
||||
.add_text("reply_to_message_id", &self.reply_to_message_id)
|
||||
.add_text("reply_markup", &self.reply_markup)
|
||||
.build(),
|
||||
)
|
||||
.await
|
||||
.await)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -21,36 +21,52 @@ impl FormBuilder {
|
|||
Self { form: Form::new() }
|
||||
}
|
||||
|
||||
/// Add the supplied key-value pair to this `FormBuilder`.
|
||||
pub async fn add<'a, T, N>(self, name: N, value: &T) -> Self
|
||||
pub fn add_text<'a, T, N>(self, name: N, value: &T) -> Self
|
||||
where
|
||||
N: Into<Cow<'a, str>>,
|
||||
T: IntoFormValue,
|
||||
T: IntoFormText,
|
||||
{
|
||||
let name = name.into().into_owned();
|
||||
match value.into_form_value() {
|
||||
Some(FormValue::Str(string)) => {
|
||||
Self { form: self.form.text(name, string) }
|
||||
}
|
||||
Some(FormValue::File(path)) => self.add_file(name, path).await,
|
||||
Some(FormValue::Memory { file_name, data }) => {
|
||||
self.add_file_from_memory(name, file_name, data)
|
||||
match value.into_form_text() {
|
||||
Some(val) => {
|
||||
Self { form: self.form.text(name.into().into_owned(), val) }
|
||||
}
|
||||
None => self,
|
||||
}
|
||||
}
|
||||
|
||||
// used in SendMediaGroup
|
||||
pub async fn add_file<'a, N>(self, name: N, path_to_file: PathBuf) -> Self
|
||||
pub async fn add_input_file<'a, N>(
|
||||
self,
|
||||
name: N,
|
||||
value: &InputFile,
|
||||
) -> tokio::io::Result<Self>
|
||||
where
|
||||
N: Into<Cow<'a, str>>,
|
||||
{
|
||||
Self {
|
||||
Ok(match value {
|
||||
InputFile::File(path) => self.add_file(name, path.clone()).await?,
|
||||
InputFile::Memory { file_name, data } => {
|
||||
self.add_file_from_memory(name, file_name.clone(), data.clone())
|
||||
}
|
||||
InputFile::Url(url) => self.add_text(name, url),
|
||||
InputFile::FileId(file_id) => self.add_text(name, file_id),
|
||||
})
|
||||
}
|
||||
|
||||
// used in SendMediaGroup
|
||||
pub async fn add_file<'a, N>(
|
||||
self,
|
||||
name: N,
|
||||
path_to_file: PathBuf,
|
||||
) -> tokio::io::Result<Self>
|
||||
where
|
||||
N: Into<Cow<'a, str>>,
|
||||
{
|
||||
Ok(Self {
|
||||
form: self.form.part(
|
||||
name.into().into_owned(),
|
||||
file_to_part(path_to_file).await,
|
||||
file_to_part(path_to_file).await?,
|
||||
),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn add_file_from_memory<'a, N>(
|
||||
|
@ -75,24 +91,18 @@ impl FormBuilder {
|
|||
}
|
||||
}
|
||||
|
||||
pub(crate) enum FormValue {
|
||||
File(PathBuf),
|
||||
Memory { file_name: String, data: Cow<'static, [u8]> },
|
||||
Str(String),
|
||||
}
|
||||
|
||||
pub(crate) trait IntoFormValue {
|
||||
fn into_form_value(&self) -> Option<FormValue>;
|
||||
pub(crate) trait IntoFormText {
|
||||
fn into_form_text(&self) -> Option<String>;
|
||||
}
|
||||
|
||||
macro_rules! impl_for_struct {
|
||||
($($name:ty),*) => {
|
||||
$(
|
||||
impl IntoFormValue for $name {
|
||||
fn into_form_value(&self) -> Option<FormValue> {
|
||||
impl IntoFormText for $name {
|
||||
fn into_form_text(&self) -> Option<String> {
|
||||
let json = serde_json::to_string(self)
|
||||
.expect("serde_json::to_string failed");
|
||||
Some(FormValue::Str(json))
|
||||
Some(json)
|
||||
}
|
||||
}
|
||||
)*
|
||||
|
@ -109,77 +119,63 @@ impl_for_struct!(
|
|||
MaskPosition
|
||||
);
|
||||
|
||||
impl<T> IntoFormValue for Option<T>
|
||||
impl<T> IntoFormText for Option<T>
|
||||
where
|
||||
T: IntoFormValue,
|
||||
T: IntoFormText,
|
||||
{
|
||||
fn into_form_value(&self) -> Option<FormValue> {
|
||||
self.as_ref().and_then(IntoFormValue::into_form_value)
|
||||
fn into_form_text(&self) -> Option<String> {
|
||||
self.as_ref().and_then(IntoFormText::into_form_text)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: fix InputMedia implementation of IntoFormValue (for now it doesn't
|
||||
// encode files :|)
|
||||
impl IntoFormValue for Vec<InputMedia> {
|
||||
fn into_form_value(&self) -> Option<FormValue> {
|
||||
impl IntoFormText for Vec<InputMedia> {
|
||||
fn into_form_text(&self) -> Option<String> {
|
||||
let json =
|
||||
serde_json::to_string(self).expect("serde_json::to_string failed");
|
||||
Some(FormValue::Str(json))
|
||||
Some(json)
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoFormValue for InputMedia {
|
||||
fn into_form_value(&self) -> Option<FormValue> {
|
||||
impl IntoFormText for InputMedia {
|
||||
fn into_form_text(&self) -> Option<String> {
|
||||
let json =
|
||||
serde_json::to_string(self).expect("serde_json::to_string failed");
|
||||
Some(FormValue::Str(json))
|
||||
Some(json)
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoFormValue for str {
|
||||
fn into_form_value(&self) -> Option<FormValue> {
|
||||
Some(FormValue::Str(self.to_owned()))
|
||||
impl IntoFormText for str {
|
||||
fn into_form_text(&self) -> Option<String> {
|
||||
Some(self.to_owned())
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoFormValue for ParseMode {
|
||||
fn into_form_value(&self) -> Option<FormValue> {
|
||||
impl IntoFormText for ParseMode {
|
||||
fn into_form_text(&self) -> Option<String> {
|
||||
let string = match self {
|
||||
ParseMode::MarkdownV2 => String::from("MarkdownV2"),
|
||||
ParseMode::HTML => String::from("HTML"),
|
||||
#[allow(deprecated)]
|
||||
ParseMode::Markdown => String::from("Markdown"),
|
||||
};
|
||||
Some(FormValue::Str(string))
|
||||
Some(string)
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoFormValue for ChatId {
|
||||
fn into_form_value(&self) -> Option<FormValue> {
|
||||
impl IntoFormText for ChatId {
|
||||
fn into_form_text(&self) -> Option<String> {
|
||||
let string = match self {
|
||||
ChatId::Id(id) => id.to_string(),
|
||||
ChatId::ChannelUsername(username) => username.clone(),
|
||||
};
|
||||
Some(FormValue::Str(string))
|
||||
Some(string)
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoFormValue for String {
|
||||
fn into_form_value(&self) -> Option<FormValue> {
|
||||
Some(FormValue::Str(self.clone()))
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoFormValue for InputFile {
|
||||
fn into_form_value(&self) -> Option<FormValue> {
|
||||
match self {
|
||||
InputFile::File(path) => Some(FormValue::File(path.clone())),
|
||||
InputFile::Memory { file_name, data } => Some(FormValue::Memory {
|
||||
file_name: file_name.clone(),
|
||||
data: data.clone(),
|
||||
}),
|
||||
InputFile::Url(url) => Some(FormValue::Str(url.clone())),
|
||||
InputFile::FileId(file_id) => Some(FormValue::Str(file_id.clone())),
|
||||
}
|
||||
impl IntoFormText for String {
|
||||
fn into_form_text(&self) -> Option<String> {
|
||||
Some(self.clone())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,3 +18,15 @@ pub trait Request {
|
|||
/// Asynchronously sends this request to Telegram and returns the result.
|
||||
async fn send(&self) -> ResponseResult<Self::Output>;
|
||||
}
|
||||
|
||||
/// Designates an API request with possibly sending file.
|
||||
#[async_trait::async_trait]
|
||||
pub trait RequestWithFile {
|
||||
/// A data structure returned if success.
|
||||
type Output;
|
||||
|
||||
/// Asynchronously sends this request to Telegram and returns the result.
|
||||
/// Returns `tokio::io::Result::Err` when trying to send file which does not
|
||||
/// exists.
|
||||
async fn send(&self) -> tokio::io::Result<ResponseResult<Self::Output>>;
|
||||
}
|
||||
|
|
|
@ -21,18 +21,16 @@ impl Decoder for FileDecoder {
|
|||
}
|
||||
}
|
||||
|
||||
pub async fn file_to_part(path_to_file: PathBuf) -> Part {
|
||||
pub async fn file_to_part(path_to_file: PathBuf) -> std::io::Result<Part> {
|
||||
let file_name =
|
||||
path_to_file.file_name().unwrap().to_string_lossy().into_owned();
|
||||
|
||||
let file = FramedRead::new(
|
||||
tokio::fs::File::open(path_to_file).await.unwrap(), /* TODO: this
|
||||
* can
|
||||
* cause panics */
|
||||
tokio::fs::File::open(path_to_file).await?,
|
||||
FileDecoder,
|
||||
);
|
||||
|
||||
Part::stream(Body::wrap_stream(file)).file_name(file_name)
|
||||
Ok(Part::stream(Body::wrap_stream(file)).file_name(file_name))
|
||||
}
|
||||
|
||||
pub fn file_from_memory_to_part(
|
||||
|
|
|
@ -171,3 +171,16 @@ fn parse_named_fields() {
|
|||
DefaultCommands::parse("/start 10 hello", "").unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn descriptions_off() {
|
||||
#[command(rename = "lowercase")]
|
||||
#[derive(BotCommand, Debug, PartialEq)]
|
||||
enum DefaultCommands {
|
||||
#[command(description = "off")]
|
||||
Start,
|
||||
Help,
|
||||
}
|
||||
|
||||
assert_eq!(DefaultCommands::descriptions(), "/help\n".to_owned());
|
||||
}
|
||||
|
|
82
tests/redis.rs
Normal file
82
tests/redis.rs
Normal file
|
@ -0,0 +1,82 @@
|
|||
use std::{
|
||||
fmt::{Debug, Display},
|
||||
future::Future,
|
||||
sync::Arc,
|
||||
};
|
||||
use teloxide::dispatching::dialogue::{
|
||||
serializer::{Bincode, CBOR, JSON},
|
||||
RedisStorage, Serializer, Storage,
|
||||
};
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_redis_json() {
|
||||
let storage =
|
||||
RedisStorage::open("redis://127.0.0.1:7777", JSON).await.unwrap();
|
||||
test_redis(storage).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_redis_bincode() {
|
||||
let storage =
|
||||
RedisStorage::open("redis://127.0.0.1:7778", Bincode).await.unwrap();
|
||||
test_redis(storage).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_redis_cbor() {
|
||||
let storage =
|
||||
RedisStorage::open("redis://127.0.0.1:7779", CBOR).await.unwrap();
|
||||
test_redis(storage).await;
|
||||
}
|
||||
|
||||
type Dialogue = String;
|
||||
|
||||
async fn test_redis<S>(storage: Arc<RedisStorage<S>>)
|
||||
where
|
||||
S: Send + Sync + Serializer<Dialogue> + 'static,
|
||||
<S as Serializer<Dialogue>>::Error: Debug + Display,
|
||||
{
|
||||
check_dialogue(
|
||||
None,
|
||||
Arc::clone(&storage).update_dialogue(1, "ABC".to_owned()),
|
||||
)
|
||||
.await;
|
||||
check_dialogue(
|
||||
None,
|
||||
Arc::clone(&storage).update_dialogue(11, "DEF".to_owned()),
|
||||
)
|
||||
.await;
|
||||
check_dialogue(
|
||||
None,
|
||||
Arc::clone(&storage).update_dialogue(256, "GHI".to_owned()),
|
||||
)
|
||||
.await;
|
||||
|
||||
// 1 - ABC, 11 - DEF, 256 - GHI
|
||||
|
||||
check_dialogue(
|
||||
"ABC",
|
||||
Arc::clone(&storage).update_dialogue(1, "JKL".to_owned()),
|
||||
)
|
||||
.await;
|
||||
check_dialogue(
|
||||
"GHI",
|
||||
Arc::clone(&storage).update_dialogue(256, "MNO".to_owned()),
|
||||
)
|
||||
.await;
|
||||
|
||||
// 1 - GKL, 11 - DEF, 256 - MNO
|
||||
|
||||
check_dialogue("JKL", Arc::clone(&storage).remove_dialogue(1)).await;
|
||||
check_dialogue("DEF", Arc::clone(&storage).remove_dialogue(11)).await;
|
||||
check_dialogue("MNO", Arc::clone(&storage).remove_dialogue(256)).await;
|
||||
}
|
||||
|
||||
async fn check_dialogue<E>(
|
||||
expected: impl Into<Option<&str>>,
|
||||
actual: impl Future<Output = Result<Option<Dialogue>, E>>,
|
||||
) where
|
||||
E: Debug,
|
||||
{
|
||||
assert_eq!(expected.into().map(ToOwned::to_owned), actual.await.unwrap())
|
||||
}
|
Loading…
Reference in a new issue