我正在使用 teloxy 一个协调游戏玩家之间信息的机器人。作为一个简化的示例,我希望玩家 A 将 task_1 标记为完成,然后我希望玩家 B 能够询问机器人 task_1 是否已完成,并让机器人做出适当的响应。
到目前为止,我已经尝试了内存存储和Redis存储,两者似乎都存储独立的状态。例如,玩家 A 可以将任务标记为完成,但当玩家 B 询问该任务的状态时,它仍然未完成。
我的机器上还没有安装 sqlite,所以还没有尝试过,但我不认为它的设置与 redis 不同(?)。
理论上我可以将机器人的状态写入磁盘上的文件,然后每次更新内部状态,但这对于我希望 teloxy 已经内置的东西来说似乎很绕?
有什么方法可以让多个用户改变机器人的相同内部状态吗?
这是代码:
db_remember.rs
示例中稍微调整的,以便我可以获取一个数字,重置该数字,并使用 \set XXX
将数字设置为新的值
src/main.rs
:
use teloxide::{
dispatching::dialogue::{
serializer::Bincode,
ErasedStorage, RedisStorage, Storage,
},
prelude::*,
utils::command::BotCommands,
};
use serde::{self, Deserialize, Serialize};
type MyDialogue = Dialogue<State, ErasedStorage<State>>;
type MyStorage = std::sync::Arc<ErasedStorage<State>>;
type HandlerResult = Result<(), Box<dyn std::error::Error + Send + Sync>>;
#[derive(Clone, Default, Serialize, Deserialize)]
pub enum State {
#[default]
Start,
GotNumber(i32),
}
#[derive(Clone, BotCommands)]
#[command(rename_rule = "lowercase", description = "These commands are supported:")]
pub enum Command {
#[command(description = "set your number.")]
Set { num: i32 },
#[command(description = "get your number.")]
Get,
#[command(description = "reset your number.")]
Reset,
}
#[tokio::main]
async fn main() {
pretty_env_logger::init();
log::info!("Starting DB remember bot...");
let bot = Bot::from_env();
let storage: MyStorage = RedisStorage::open("redis://127.0.0.1:6379", Bincode).await.unwrap().erase();
let handler = Update::filter_message()
.enter_dialogue::<Message, ErasedStorage<State>, State>()
.branch(dptree::case![State::Start].endpoint(start))
.branch(
dptree::case![State::GotNumber(n)]
.branch(dptree::entry().filter_command::<Command>().endpoint(got_number))
.branch(dptree::endpoint(invalid_command)),
);
Dispatcher::builder(bot, handler)
.dependencies(dptree::deps![storage])
.enable_ctrlc_handler()
.build()
.dispatch()
.await;
}
async fn start(bot: Bot, dialogue: MyDialogue, msg: Message) -> HandlerResult {
match msg.text().map(|text| text.parse::<i32>()) {
Some(Ok(n)) => {
log::info!("[{:?}] Number set to {n}", msg.chat.username());
dialogue.update(State::GotNumber(n)).await?;
bot.send_message(
msg.chat.id,
format!("Remembered number {n}. Now use /get or /reset."),
)
.await?;
}
_ => {
bot.send_message(msg.chat.id, "Please, send me a number.").await?;
}
}
Ok(())
}
async fn got_number(
bot: Bot,
dialogue: MyDialogue,
num: i32, // Available from `State::GotNumber`.
msg: Message,
cmd: Command,
) -> HandlerResult {
let old_num = num;
match cmd {
Command::Set { num } => {
log::info!("[{:?}] Number changed from {} to {}", msg.chat.username(), old_num, num);
dialogue.update(State::GotNumber(num)).await?;
bot.send_message(msg.chat.id, format!("Set your number from {} to {}", old_num, num)).await?;
}
Command::Get => {
bot.send_message(msg.chat.id, format!("Here is your number: {num}.")).await?;
}
Command::Reset => {
dialogue.reset().await?;
bot.send_message(msg.chat.id, "Number reset.").await?;
}
}
Ok(())
}
async fn invalid_command(bot: Bot, msg: Message) -> HandlerResult {
bot.send_message(msg.chat.id, "Please, send /get or /reset.").await?;
Ok(())
}
Cargo.toml
:
[package]
name = "tmp"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
teloxide = { version = "0.12", features = ["macros", "redis-storage", "bincode-serializer"] }
log = "0.4"
pretty_env_logger = "0.4"
tokio = { version = "1.8", features = ["rt-multi-thread", "macros"] }
serde_yaml = "0.9.17"
serde = "1.0.152"
这是由 WaffleLapkin 在 Teloxy github 讨论页面上回答的:
teloxy 中的对话存储存储每个聊天的状态,如果您需要其他内容,则需要直接使用数据库。
所以我最终使用 Redis 数据库来存储游戏状态,并使用常规内存来存储 teloxy 的状态。比如:
// Cargo.toml: redis = { version = "*" }
extern crate redis;
use redis::{Commands, Connection};
/// Establish a connection to the redis database,
/// used for sharing Game state across
/// different instances of the bot
fn connect_to_redis() -> Result<Connection, Box<dyn std::error::Error + Sync + Send>> {
let client = redis::Client::open("redis://127.0.0.1/")?;
let conn = client.get_connection()?;
// Cargo.toml: log = "0.4"
log::info!("Redis connection established");
Ok(conn)
}
但是你也不能直接将 Rust 结构存储到 Redis 中,所以我使用
serde_yaml
来序列化游戏状态:
fn set_game_state(
conn: &mut Connection,
game: Game,
) -> Result<(), Box<dyn Error + Send + Sync>> {
let game_key = "game_state";
conn.set::<&str, String, ()>(game_key, serde_yaml::to_string(&game)?)?;
Ok(())
}
然后将其全部作为一个长字符串存储在数据库中的单个键中。
检索游戏状态是类似的操作,但我还添加了一些逻辑,以便从现有的“模板”YAML 文件创建游戏
game.yaml
(如果密钥不存在):
fn get_game_state(conn: &mut Connection) -> Result<Game, Box<dyn Error + Send + Sync>> {
let game_state_key = "game_state";
let game_state: Game = if conn.exists(game_state_key)? {
log::debug!("Getting game state");
conn.get::<&str, String>(game_state_key)
.and_then(|game_state| Ok(serde_yaml::from_str::<Game>(&game_state).unwrap().into()))?
} else {
log::debug!("Creating game state");
let s = Game::new("challenges.yaml")?;
let serialised = serde_yaml::to_string(&s)?;
conn.set::<&str, String, ()>(game_state_key, serialised)?;
s
};
Ok(game_state)
}
有点矫枉过正,但确实有效。