mirror of
https://github.com/teloxide/teloxide.git
synced 2025-03-22 06:45:37 +01:00
Add Redis storage & example bot using it
This commit is contained in:
parent
bef3464fdc
commit
798102a7d7
8 changed files with 389 additions and 3 deletions
|
@ -23,7 +23,10 @@ authors = [
|
||||||
[badges]
|
[badges]
|
||||||
maintenance = { status = "actively-developed" }
|
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]
|
[dependencies]
|
||||||
serde_json = "1.0.44"
|
serde_json = "1.0.44"
|
||||||
|
@ -45,6 +48,10 @@ futures = "0.3.1"
|
||||||
pin-project = "0.4.6"
|
pin-project = "0.4.6"
|
||||||
serde_with_macros = "1.0.1"
|
serde_with_macros = "1.0.1"
|
||||||
|
|
||||||
|
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.2.1"
|
teloxide-macros = "0.2.1"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
|
|
@ -41,7 +41,7 @@ enum FavouriteMusic {
|
||||||
|
|
||||||
impl FavouriteMusic {
|
impl FavouriteMusic {
|
||||||
fn markup() -> ReplyKeyboardMarkup {
|
fn markup() -> ReplyKeyboardMarkup {
|
||||||
ReplyKeyboardMarkup::default().append_row(vec![
|
ReplyKeyboardMarkup::default().one_time_keyboard(true).append_row(vec![
|
||||||
KeyboardButton::new("Rock"),
|
KeyboardButton::new("Rock"),
|
||||||
KeyboardButton::new("Metal"),
|
KeyboardButton::new("Metal"),
|
||||||
KeyboardButton::new("Pop"),
|
KeyboardButton::new("Pop"),
|
||||||
|
|
20
examples/dialogue_bot_redis/Cargo.toml
Normal file
20
examples/dialogue_bot_redis/Cargo.toml
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
[package]
|
||||||
|
name = "dialogue_bot_redis"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors = ["Temirkhan Myrzamadi <hirrolot@gmail.com>"]
|
||||||
|
edition = "2018"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
log = "0.4.8"
|
||||||
|
tokio = "0.2.9"
|
||||||
|
pretty_env_logger = "0.4.0"
|
||||||
|
smart-default = "0.6.0"
|
||||||
|
parse-display = "0.1.1"
|
||||||
|
# You can also choose "cbor-serializer" or built-in JSON serializer
|
||||||
|
teloxide = { path = "../../", features = ["redis-storage", "bincode-serializer"] }
|
||||||
|
serde = "1.0.104"
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
lto = true
|
210
examples/dialogue_bot_redis/src/main.rs
Normal file
210
examples/dialogue_bot_redis/src/main.rs
Normal file
|
@ -0,0 +1,210 @@
|
||||||
|
// This is a bot that asks your full name, your age, your favourite kind of
|
||||||
|
// music and sends all the gathered information back.
|
||||||
|
//
|
||||||
|
// # Example
|
||||||
|
// ```
|
||||||
|
// - Let's start! First, what's your full name?
|
||||||
|
// - Luke Skywalker
|
||||||
|
// - What a wonderful name! Your age?
|
||||||
|
// - 26
|
||||||
|
// - Good. Now choose your favourite music
|
||||||
|
// *A keyboard of music kinds is displayed*
|
||||||
|
// *You select Metal*
|
||||||
|
// - Metal
|
||||||
|
// - Fine. Your full name: Luke Skywalker, your age: 26, your favourite music: Metal
|
||||||
|
// ```
|
||||||
|
|
||||||
|
#![allow(clippy::trivial_regex)]
|
||||||
|
|
||||||
|
#[macro_use]
|
||||||
|
extern crate smart_default;
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use teloxide::{
|
||||||
|
dispatching::dialogue::{RedisStorage, Serializer, Storage},
|
||||||
|
prelude::*,
|
||||||
|
types::{KeyboardButton, ReplyKeyboardMarkup},
|
||||||
|
};
|
||||||
|
|
||||||
|
use parse_display::{Display, FromStr};
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// [Favourite music kinds]
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Display, FromStr)]
|
||||||
|
enum FavouriteMusic {
|
||||||
|
Rock,
|
||||||
|
Metal,
|
||||||
|
Pop,
|
||||||
|
Other,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FavouriteMusic {
|
||||||
|
fn markup() -> ReplyKeyboardMarkup {
|
||||||
|
ReplyKeyboardMarkup::default().one_time_keyboard(true).append_row(vec![
|
||||||
|
KeyboardButton::new("Rock"),
|
||||||
|
KeyboardButton::new("Metal"),
|
||||||
|
KeyboardButton::new("Pop"),
|
||||||
|
KeyboardButton::new("Other"),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// [A type-safe finite automaton]
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
#[derive(Clone, Serialize, Deserialize)]
|
||||||
|
struct ReceiveAgeState {
|
||||||
|
full_name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Serialize, Deserialize)]
|
||||||
|
struct ReceiveFavouriteMusicState {
|
||||||
|
data: ReceiveAgeState,
|
||||||
|
age: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Display)]
|
||||||
|
#[display(
|
||||||
|
"Your full name: {data.data.full_name}, your age: {data.age}, your \
|
||||||
|
favourite music: {favourite_music}"
|
||||||
|
)]
|
||||||
|
struct ExitState {
|
||||||
|
data: ReceiveFavouriteMusicState,
|
||||||
|
favourite_music: FavouriteMusic,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(SmartDefault, Serialize, Deserialize)]
|
||||||
|
enum Dialogue {
|
||||||
|
#[default]
|
||||||
|
Start,
|
||||||
|
ReceiveFullName,
|
||||||
|
ReceiveAge(ReceiveAgeState),
|
||||||
|
ReceiveFavouriteMusic(ReceiveFavouriteMusicState),
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// [Control a dialogue]
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
type Cx<State> = DialogueDispatcherHandlerCx<
|
||||||
|
Message,
|
||||||
|
State,
|
||||||
|
<RedisStorage as Storage<Dialogue>>::Error,
|
||||||
|
>;
|
||||||
|
type Res = ResponseResult<DialogueStage<Dialogue>>;
|
||||||
|
|
||||||
|
async fn start(cx: Cx<()>) -> Res {
|
||||||
|
cx.answer("Let's start! First, what's your full name?").send().await?;
|
||||||
|
next(Dialogue::ReceiveFullName)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn full_name(cx: Cx<()>) -> Res {
|
||||||
|
match cx.update.text() {
|
||||||
|
None => {
|
||||||
|
cx.answer("Please, send me a text message!").send().await?;
|
||||||
|
next(Dialogue::ReceiveFullName)
|
||||||
|
}
|
||||||
|
Some(full_name) => {
|
||||||
|
cx.answer("What a wonderful name! Your age?").send().await?;
|
||||||
|
next(Dialogue::ReceiveAge(ReceiveAgeState {
|
||||||
|
full_name: full_name.to_owned(),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn age(cx: Cx<ReceiveAgeState>) -> Res {
|
||||||
|
match cx.update.text().unwrap().parse() {
|
||||||
|
Ok(age) => {
|
||||||
|
cx.answer("Good. Now choose your favourite music:")
|
||||||
|
.reply_markup(FavouriteMusic::markup())
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
next(Dialogue::ReceiveFavouriteMusic(ReceiveFavouriteMusicState {
|
||||||
|
data: cx.dialogue.unwrap(),
|
||||||
|
age,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
cx.answer("Oh, please, enter a number!").send().await?;
|
||||||
|
next(Dialogue::ReceiveAge(cx.dialogue.unwrap()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn favourite_music(cx: Cx<ReceiveFavouriteMusicState>) -> Res {
|
||||||
|
match cx.update.text().unwrap().parse() {
|
||||||
|
Ok(favourite_music) => {
|
||||||
|
cx.answer(format!(
|
||||||
|
"Fine. {}",
|
||||||
|
ExitState {
|
||||||
|
data: cx.dialogue.as_ref().unwrap().clone(),
|
||||||
|
favourite_music
|
||||||
|
}
|
||||||
|
))
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
exit()
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
cx.answer("Oh, please, enter from the keyboard!").send().await?;
|
||||||
|
next(Dialogue::ReceiveFavouriteMusic(cx.dialogue.unwrap()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_message(cx: Cx<Dialogue>) -> Res {
|
||||||
|
let DialogueDispatcherHandlerCx { bot, update, dialogue } = cx;
|
||||||
|
match dialogue.unwrap() {
|
||||||
|
Dialogue::Start => {
|
||||||
|
start(DialogueDispatcherHandlerCx::new(bot, update, ())).await
|
||||||
|
}
|
||||||
|
Dialogue::ReceiveFullName => {
|
||||||
|
full_name(DialogueDispatcherHandlerCx::new(bot, update, ())).await
|
||||||
|
}
|
||||||
|
Dialogue::ReceiveAge(s) => {
|
||||||
|
age(DialogueDispatcherHandlerCx::new(bot, update, s)).await
|
||||||
|
}
|
||||||
|
Dialogue::ReceiveFavouriteMusic(s) => {
|
||||||
|
favourite_music(DialogueDispatcherHandlerCx::new(bot, update, s))
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// [Run!]
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
run().await;
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn run() {
|
||||||
|
teloxide::enable_logging!();
|
||||||
|
log::info!("Starting dialogue_bot!");
|
||||||
|
|
||||||
|
let bot = Bot::from_env();
|
||||||
|
|
||||||
|
Dispatcher::new(bot)
|
||||||
|
.messages_handler(DialogueDispatcher::with_storage(
|
||||||
|
|cx| async move {
|
||||||
|
handle_message(cx).await.expect("Something wrong with the bot!")
|
||||||
|
},
|
||||||
|
Arc::new(
|
||||||
|
// You can also choose Serializer::JSON or Serializer::Bincode
|
||||||
|
// All serializer but JSON require enabling feature "serializer-<name>",
|
||||||
|
// e. g. "serializer-cbor" or "serializer-bincode"
|
||||||
|
RedisStorage::open("redis://127.0.0.1:6379", Serializer::CBOR)
|
||||||
|
.unwrap(),
|
||||||
|
),
|
||||||
|
))
|
||||||
|
.dispatch()
|
||||||
|
.await;
|
||||||
|
}
|
|
@ -54,4 +54,6 @@ pub use dialogue_dispatcher_handler::DialogueDispatcherHandler;
|
||||||
pub use dialogue_dispatcher_handler_cx::DialogueDispatcherHandlerCx;
|
pub use dialogue_dispatcher_handler_cx::DialogueDispatcherHandlerCx;
|
||||||
pub use dialogue_stage::{exit, next, DialogueStage};
|
pub use dialogue_stage::{exit, next, DialogueStage};
|
||||||
pub use get_chat_id::GetChatId;
|
pub use get_chat_id::GetChatId;
|
||||||
pub use storage::{InMemStorage, Storage};
|
#[cfg(feature = "redis-storage")]
|
||||||
|
pub use storage::RedisStorage;
|
||||||
|
pub use storage::{InMemStorage, Serializer, Storage};
|
||||||
|
|
|
@ -1,7 +1,16 @@
|
||||||
|
pub mod serializer;
|
||||||
|
|
||||||
mod in_mem_storage;
|
mod in_mem_storage;
|
||||||
|
|
||||||
|
#[cfg(feature = "redis-storage")]
|
||||||
|
mod redis_storage;
|
||||||
|
|
||||||
use futures::future::BoxFuture;
|
use futures::future::BoxFuture;
|
||||||
|
|
||||||
pub use in_mem_storage::InMemStorage;
|
pub use in_mem_storage::InMemStorage;
|
||||||
|
#[cfg(feature = "redis-storage")]
|
||||||
|
pub use redis_storage::RedisStorage;
|
||||||
|
pub use serializer::Serializer;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
/// A storage of dialogues.
|
/// A storage of dialogues.
|
||||||
|
|
85
src/dispatching/dialogue/storage/redis_storage.rs
Normal file
85
src/dispatching/dialogue/storage/redis_storage.rs
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
use super::{
|
||||||
|
serializer::{self, Serializer},
|
||||||
|
Storage,
|
||||||
|
};
|
||||||
|
use futures::future::BoxFuture;
|
||||||
|
use redis::{AsyncCommands, FromRedisValue, IntoConnectionInfo};
|
||||||
|
use serde::{de::DeserializeOwned, Serialize};
|
||||||
|
use std::sync::Arc;
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum Error {
|
||||||
|
#[error("{0}")]
|
||||||
|
SerdeError(#[from] serializer::Error),
|
||||||
|
#[error("error from Redis: {0}")]
|
||||||
|
RedisError(#[from] redis::RedisError),
|
||||||
|
}
|
||||||
|
|
||||||
|
type Result<T, E = Error> = std::result::Result<T, E>;
|
||||||
|
|
||||||
|
pub struct RedisStorage {
|
||||||
|
client: redis::Client,
|
||||||
|
serializer: Serializer,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RedisStorage {
|
||||||
|
pub fn open(
|
||||||
|
url: impl IntoConnectionInfo,
|
||||||
|
serializer: Serializer,
|
||||||
|
) -> Result<Self> {
|
||||||
|
Ok(Self { client: redis::Client::open(url)?, serializer })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<D> Storage<D> for RedisStorage
|
||||||
|
where
|
||||||
|
D: Send + Serialize + DeserializeOwned + 'static,
|
||||||
|
{
|
||||||
|
type Error = 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>>> {
|
||||||
|
Box::pin(async move {
|
||||||
|
let mut conn = self.client.get_async_connection().await?;
|
||||||
|
let res = redis::pipe()
|
||||||
|
.atomic()
|
||||||
|
.get(chat_id)
|
||||||
|
.del(chat_id).ignore()
|
||||||
|
.query_async::<_, redis::Value>(&mut conn)
|
||||||
|
.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))
|
||||||
|
.transpose()?
|
||||||
|
)
|
||||||
|
},
|
||||||
|
_ => unreachable!()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_dialogue(
|
||||||
|
self: Arc<Self>,
|
||||||
|
chat_id: i64,
|
||||||
|
dialogue: D,
|
||||||
|
) -> BoxFuture<'static, Result<Option<D>>> {
|
||||||
|
Box::pin(async move {
|
||||||
|
let mut conn = self.client.get_async_connection().await?;
|
||||||
|
let dialogue = self.serializer.serialize(&dialogue)?;
|
||||||
|
Ok(conn
|
||||||
|
.getset::<_, Vec<u8>, Option<Vec<u8>>>(chat_id, dialogue)
|
||||||
|
.await?
|
||||||
|
.map(|d| self.serializer.deserialize(&d))
|
||||||
|
.transpose()?)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
53
src/dispatching/dialogue/storage/serializer.rs
Normal file
53
src/dispatching/dialogue/storage/serializer.rs
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
use serde::{de::DeserializeOwned, ser::Serialize};
|
||||||
|
use thiserror::Error;
|
||||||
|
use Serializer::*;
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum Error {
|
||||||
|
#[error("failed parsing/serializing JSON: {0}")]
|
||||||
|
JSONError(#[from] serde_json::Error),
|
||||||
|
#[cfg(feature = "cbor-serializer")]
|
||||||
|
#[error("failed parsing/serializing CBOR: {0}")]
|
||||||
|
CBORError(#[from] serde_cbor::Error),
|
||||||
|
#[cfg(feature = "bincode-serializer")]
|
||||||
|
#[error("failed parsing/serializing Bincode: {0}")]
|
||||||
|
BincodeError(#[from] bincode::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
type Result<T, E = Error> = std::result::Result<T, E>;
|
||||||
|
|
||||||
|
pub enum Serializer {
|
||||||
|
JSON,
|
||||||
|
#[cfg(feature = "cbor-serializer")]
|
||||||
|
CBOR,
|
||||||
|
#[cfg(feature = "bincode-serializer")]
|
||||||
|
Bincode,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Serializer {
|
||||||
|
pub fn serialize<D>(&self, val: &D) -> Result<Vec<u8>>
|
||||||
|
where
|
||||||
|
D: Serialize,
|
||||||
|
{
|
||||||
|
Ok(match self {
|
||||||
|
JSON => serde_json::to_vec(val)?,
|
||||||
|
#[cfg(feature = "cbor-serializer")]
|
||||||
|
CBOR => serde_cbor::to_vec(val)?,
|
||||||
|
#[cfg(feature = "bincode-serializer")]
|
||||||
|
Bincode => bincode::serialize(val)?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deserialize<'de, D>(&self, data: &'de [u8]) -> Result<D>
|
||||||
|
where
|
||||||
|
D: DeserializeOwned,
|
||||||
|
{
|
||||||
|
Ok(match self {
|
||||||
|
JSON => serde_json::from_slice(data)?,
|
||||||
|
#[cfg(feature = "cbor-serializer")]
|
||||||
|
CBOR => serde_cbor::from_slice(data)?,
|
||||||
|
#[cfg(feature = "bincode-serializer")]
|
||||||
|
Bincode => bincode::deserialize(data)?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue