-
Notifications
You must be signed in to change notification settings - Fork 119
/
inline-pagination.rs
169 lines (146 loc) · 5.49 KB
/
inline-pagination.rs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
//! Example to showcase how to achieve pagination with inline buttons.
//!
//! The `TG_ID` and `TG_HASH` environment variables must be set (learn how to do it for
//! [Windows](https://ss64.com/nt/set.html) or [Linux](https://ss64.com/bash/export.html))
//! to Telegram's API ID and API hash respectively.
//!
//! Then, run it as:
//!
//! ```sh
//! cargo run --example echo -- BOT_TOKEN
//! ```
//!
//! In order to achieve pagination, the button data must contain enough information for the code
//! to determine which "offset" should it use when querying additional data.
//!
//! For this example, the button contains the last two values of the fibonacci sequence where it
//! last left off, separated by a comma. This way, the code can resume.
//!
//! If there is no comma, then there is a single number, which we use to know what was clicked.
//!
//! If it's the special string "done", then we know we've reached the end (there is a limit to
//! how much data a button's payload can contain, and to keep it simple, we're storing it inline
//! in decimal, so the numbers can't get too large).
use std::env;
use std::pin::pin;
use futures::future::{select, Either};
use simple_logger::SimpleLogger;
use tokio::{runtime, task};
use grammers_client::session::Session;
use grammers_client::{button, reply_markup, Client, Config, InputMessage, Update};
type Result = std::result::Result<(), Box<dyn std::error::Error>>;
const SESSION_FILE: &str = "inline-pagination.session";
const NUMBERS_PER_PAGE: usize = 4;
// https://core.telegram.org/bots/api#inlinekeyboardbutton
const MAX_PAYLOAD_DATA_LEN: usize = 64;
/// Generate the inline keyboard reply markup with a few more numbers from the sequence.
fn fib_markup(mut a: u128, mut b: u128) -> reply_markup::Inline {
let mut rows = Vec::with_capacity(NUMBERS_PER_PAGE + 1);
for _ in 0..NUMBERS_PER_PAGE {
let text = a.to_string();
rows.push(vec![button::inline(&text, text.as_bytes())]);
let bb = b;
b += a;
a = bb;
}
let next = format!("{a},{b}");
if next.len() > MAX_PAYLOAD_DATA_LEN {
rows.push(vec![button::inline("I'm satisfied!!", b"done".to_vec())]);
} else {
rows.push(vec![
button::inline("Restart!", b"0,1".to_vec()),
button::inline("More!", format!("{a},{b}").into_bytes()),
]);
}
reply_markup::inline(rows)
}
async fn handle_update(_client: Client, update: Update) -> Result {
match update {
Update::NewMessage(message) if message.text() == "/start" => {
message
.respond(InputMessage::text("Here's a fibonacci").reply_markup(&fib_markup(0, 1)))
.await?;
}
Update::CallbackQuery(query) => {
let data = std::str::from_utf8(query.data()).unwrap();
println!("Got callback query for {data}");
// First check special-case.
if data == "done" {
query.answer().edit("Glad you liked it 👍").await?;
return Ok(());
}
// Otherwise get the stored number(s).
let mut parts = data.split(',');
let a = parts.next().unwrap().parse::<u128>().unwrap();
if let Some(b) = parts.next() {
let os = (0..b.len()).map(|_| 'o').collect::<String>();
let b = b.parse::<u128>().unwrap();
query
.answer()
.edit(
InputMessage::from(format!("S{os} much fibonacci 🔢"))
.reply_markup(&fib_markup(a, b)),
)
.await?;
} else if a % 2 == 0 {
query.answer().text("Even that's a number!").send().await?;
} else {
query.answer().alert("That's odd…").send().await?;
}
}
_ => {}
}
Ok(())
}
async fn async_main() -> Result {
SimpleLogger::new()
.with_level(log::LevelFilter::Debug)
.init()
.unwrap();
let api_id = env!("TG_ID").parse().expect("TG_ID invalid");
let api_hash = env!("TG_HASH").to_string();
let token = env::args().nth(1).expect("token missing");
println!("Connecting to Telegram...");
let client = Client::connect(Config {
session: Session::load_file_or_create(SESSION_FILE)?,
api_id,
api_hash: api_hash.clone(),
params: Default::default(),
})
.await?;
println!("Connected!");
if !client.is_authorized().await? {
println!("Signing in...");
client.bot_sign_in(&token).await?;
client.session().save_to_file(SESSION_FILE)?;
println!("Signed in!");
}
println!("Waiting for messages...");
loop {
let exit = pin!(async { tokio::signal::ctrl_c().await });
let upd = pin!(async { client.next_update().await });
let update = match select(exit, upd).await {
Either::Left(_) => {
println!("Exiting...");
break;
}
Either::Right((u, _)) => u?,
};
let handle = client.clone();
task::spawn(async move {
if let Err(e) = handle_update(handle, update).await {
eprintln!("Error handling updates!: {e}")
}
});
}
println!("Saving session file...");
client.session().save_to_file(SESSION_FILE)?;
Ok(())
}
fn main() -> Result {
runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap()
.block_on(async_main())
}