Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Conversation API

The Conversation type provides a high-level, blocking-style interface for multi-step bot flows: send a question, wait for the answer, send the next question, and so on - all within a single async fn, without manually tracking state.

Conversation wraps a mutable reference to an UpdateStream and filters updates to a single peer. Updates from other peers are buffered internally and can be retrieved with drain_buffered().

Quick start

#![allow(unused)]
fn main() {
use std::time::Duration;
use ferogram::conversation::Conversation;

// In a handler or task that already owns the update stream:
let mut conv = Conversation::new(&client, &mut stream, "@username").await?;

conv.ask("What is your name?").await?;
let name_msg = conv.get_response(Duration::from_secs(60)).await?;
let name = name_msg.text().unwrap_or("unknown").to_string();

conv.ask(format!("Nice to meet you, {}! How old are you?", name)).await?;
let age_msg = conv.get_response(Duration::from_secs(60)).await?;
let age = age_msg.text().unwrap_or("?").to_string();

conv.respond(format!("Got it: {} is {} years old.", name, age)).await?;
}

Creating a Conversation

#![allow(unused)]
fn main() {
use ferogram::conversation::Conversation;

let mut conv = Conversation::new(&client, &mut stream, peer).await?;
}

peer accepts anything that implements Into<PeerRef>: a &str username, a numeric ID, or a resolved tl::enums::Peer.


Sending messages

MethodDescription
conv.ask(text).awaitSend a message and return the sent IncomingMessage
conv.respond(text).awaitAlias for ask

Both accept any Into<String>.


Waiting for responses

MethodDescription
conv.get_response(deadline).awaitWait for the next message from the peer
conv.wait_click(deadline).awaitWait for the peer to press an inline button
conv.wait_read(deadline).awaitWait until messages are read (any non-message update from peer)
conv.ask_and_wait(text, deadline).awaitSend a message and immediately wait for the reply

deadline is a std::time::Duration. If no response arrives within the deadline, the method returns ConversationError::Timeout.

Non-matching updates (from other peers, or update types other than NewMessage / CallbackQuery) are buffered. Retrieve them with:

#![allow(unused)]
fn main() {
let leftover: Vec<Update> = conv.drain_buffered();
}

Error handling

#![allow(unused)]
fn main() {
use ferogram::conversation::ConversationError;

match conv.get_response(Duration::from_secs(30)).await {
    Ok(msg) => { /* process msg */ }
    Err(ConversationError::Timeout(d)) => {
        conv.respond("You took too long! Please try again.").await.ok();
    }
    Err(ConversationError::StreamClosed) => { /* bot is shutting down */ }
    Err(ConversationError::Invocation(e)) => { return Err(e.into()); }
}
}

Button interaction example

#![allow(unused)]
fn main() {
use std::time::Duration;
use ferogram::{InputMessage, keyboard::{Button, InlineKeyboard}};
use ferogram::conversation::Conversation;

let mut conv = Conversation::new(&client, &mut stream, peer).await?;

let kb = InlineKeyboard::new()
    .row([
        Button::callback("✅ Yes", b"yes"),
        Button::callback("❌ No",  b"no"),
    ]);

conv.ask(InputMessage::text("Confirm your order?").keyboard(kb)).await?;

let click = conv.wait_click(Duration::from_secs(120)).await?;
match click.data().unwrap_or("") {
    "yes" => conv.respond("Order confirmed!").await?,
    _     => conv.respond("Order cancelled.").await?,
};
}

Integrating with the dispatcher

Because Conversation borrows &mut UpdateStream exclusively, you typically use it in a handler that was given the stream, or spin up a dedicated task:

#![allow(unused)]
fn main() {
use std::sync::Arc;
use tokio::sync::Mutex;

// Give one user their own conversation task
let stream = Arc::new(Mutex::new(client.stream_updates()));

// … in a handler triggered by /start:
let client2 = client.clone();
let stream2 = stream.clone();
let user_peer = msg.peer_id().cloned().unwrap();

tokio::spawn(async move {
    let mut locked = stream2.lock().await;
    if let Ok(mut conv) = Conversation::new(&client2, &mut locked, user_peer).await {
        run_onboarding(&mut conv).await.ok();
    }
});
}

For multi-user bots, the FSM approach is usually a better fit - see FSM. Use Conversation when the flow is short and you need the simplicity of sequential await calls.


Full example: simple registration flow

#![allow(unused)]
fn main() {
use std::time::Duration;
use ferogram::conversation::{Conversation, ConversationError};

async fn registration_flow(
    client: &ferogram::Client,
    stream: &mut ferogram::UpdateStream,
    peer: ferogram::tl::enums::Peer,
) -> Result<(), ConversationError> {
    let mut conv = Conversation::new(client, stream, peer).await?;
    let timeout = Duration::from_secs(120);

    conv.ask("Welcome! Please enter your first name:").await?;
    let first = conv.get_response(timeout).await?;
    let first_name = first.text().unwrap_or("").trim().to_string();
    if first_name.is_empty() {
        conv.respond("Name cannot be empty. Please /start again.").await?;
        return Ok(());
    }

    conv.ask("Great! Now your email address:").await?;
    let email_msg = conv.get_response(timeout).await?;
    let email = email_msg.text().unwrap_or("").trim().to_string();

    conv.respond(format!(
        "Registered: {} <{}>. Welcome aboard!", first_name, email
    )).await?;

    Ok(())
}
}