Lines of
src/tg_bot.rs
from check-in be0b8602d1
that are changed by the sequence of edits moving toward
check-in d8c1d259a2:
1: use crate::{
2: Arc,
3: Mutex,
4: core::FeedList,
5: };
6:
7: use std::{
8: borrow::Cow,
9: fmt,
10: time::Duration,
11: };
12:
13: use serde::{
14: Deserialize,
15: Serialize,
16: };
17: use smol::Timer;
18: use stacked_errors::{
19: bail,
20: Result,
21: StackableErr,
22: };
23: use tgbot::{
24: api::{
25: Client,
26: ExecuteError
27: },
28: types::{
29: AnswerCallbackQuery,
30: Bot,
31: ChatPeerId,
32: EditMessageResult,
33: EditMessageText,
34: GetBot,
35: InlineKeyboardButton,
36: InlineKeyboardMarkup,
37: Message,
38: ParseMode,
39: SendMessage,
40: },
41: };
42:
43: const CB_VERSION: u8 = 0;
44:
45: #[derive(Serialize, Deserialize, Debug)]
46: pub enum Callback {
47: // Edit one feed (version, name)
48: Edit(u8, String),
49: // List all feeds (version, name to show, page number)
50: List(u8, String, usize),
51: // Show root menu (version)
52: Menu(u8),
53: }
54:
55: impl Callback {
56: pub fn edit <S>(text: S) -> Callback
57: where S: Into<String> {
58: Callback::Edit(CB_VERSION, text.into())
59: }
60:
61: pub fn list <S>(text: S, page: usize) -> Callback
62: where S: Into<String> {
63: Callback::List(CB_VERSION, text.into(), page)
64: }
65:
66: pub fn menu () -> Callback {
67: Callback::Menu(CB_VERSION)
68: }
69:
70: fn version (&self) -> u8 {
71: match self {
72: Callback::Edit(version, .. ) => *version,
73: Callback::List(version, .. ) => *version,
74: Callback::Menu(version) => *version,
75: }
76: }
77: }
78:
79: impl fmt::Display for Callback {
80: fn fmt (&self, f: &mut fmt::Formatter) -> fmt::Result {
81: f.write_str(&toml::to_string(self).map_err(|_| fmt::Error)?)
82: }
83: }
84:
85: /// Produce new Keyboard Markup from current Callback
86: pub async fn get_kb (cb: &Callback, feeds: &Arc<Mutex<FeedList>>) -> Result<InlineKeyboardMarkup> {
87: if cb.version() != CB_VERSION {
88: bail!("Wrong callback version.");
89: }
90: let mark = match cb {
91: Callback::Edit(_, _name) => { // XXX edit missing
92: let kb: Vec<Vec<InlineKeyboardButton>> = vec![];
93: InlineKeyboardMarkup::from(kb)
94: },
95: Callback::List(_, name, page) => {
96: let mut kb = vec![];
97: let feeds = feeds.lock_arc().await;
98: let long = feeds.len() > 6;
99: let (start, end) = if long {
100: (page * 5, 5 + page * 5)
101: } else {
102: (0, 6)
103: };
104: let mut i = 0;
105: if name.is_empty() {
106: let feed_iter = feeds.iter().skip(start);
107: for (id, name) in feed_iter {
108: kb.push(vec![
109: InlineKeyboardButton::for_callback_data(
110: format!("{}. {}", id, name),
111: Callback::edit(name).to_string()),
112: ]);
113: i += 1;
114: if i == end { break }
115: }
116: } else {
117: let mut found = false;
118: let mut first_page = None;
119: for (id, feed_name) in feeds.iter() {
120: if name == feed_name {
121: found = true;
122: }
123: i += 1;
124: kb.push(vec![
125: InlineKeyboardButton::for_callback_data(
126: format!("{}. {}", id, feed_name),
127: Callback::list("xxx", *page).to_string()), // XXX edit
128: ]);
129: if i == end {
130: // page complete, if found we got the right page, if not - reset and
131: // continue.
132: if found {
133: break
134: } else {
135: if first_page.is_none() {
136: first_page = Some(kb);
137: }
138: kb = vec![];
139: i = 0
140: }
141: }
142: }
143: if !found {
144: // name not found, showing first page
145: kb = first_page.unwrap_or_default();
146: }
147: }
148: if long {
149: kb.push(vec![
150: InlineKeyboardButton::for_callback_data("<<",
151: Callback::list("", if *page == 0 { *page } else { page - 1 } ).to_string()),
152: InlineKeyboardButton::for_callback_data(">>",
be0b8602d1 2026-04-18 153: Callback::list("", page + 1).to_string()),
154: ]);
155: }
156: InlineKeyboardMarkup::from(kb)
157: },
158: Callback::Menu(_) => {
159: let kb = vec![
160: vec![
161: InlineKeyboardButton::for_callback_data(
162: "Add new channel",
163: Callback::menu().to_string()), // new XXX
164: ],
165: vec![
166: InlineKeyboardButton::for_callback_data(
167: "List channels",
168: Callback::list("", 0).to_string()),
169: ],
170: ];
171: InlineKeyboardMarkup::from(kb)
172: },
173: };
174: Ok(mark)
175: }
176:
177: pub enum MyMessage <'a> {
178: Html { text: Cow<'a, str> },
179: HtmlTo { text: Cow<'a, str>, to: ChatPeerId },
180: HtmlToKb { text: Cow<'a, str>, to: ChatPeerId, kb: InlineKeyboardMarkup },
181: }
182:
183: impl MyMessage <'_> {
184: pub fn html <'a, S> (text: S) -> MyMessage<'a>
185: where S: Into<Cow<'a, str>> {
186: let text = text.into();
187: MyMessage::Html { text }
188: }
189:
190: pub fn html_to <'a, S> (text: S, to: ChatPeerId) -> MyMessage<'a>
191: where S: Into<Cow<'a, str>> {
192: let text = text.into();
193: MyMessage::HtmlTo { text, to }
194: }
195:
196: pub fn html_to_kb <'a, S> (text: S, to: ChatPeerId, kb: InlineKeyboardMarkup) -> MyMessage<'a>
197: where S: Into<Cow<'a, str>> {
198: let text = text.into();
199: MyMessage::HtmlToKb { text, to, kb }
200: }
201:
be0b8602d1 2026-04-18 202: fn req (&self, tg: &Tg) -> Result<SendMessage> {
be0b8602d1 2026-04-18 203: Ok(match self {
204: MyMessage::Html { text } =>
205: SendMessage::new(tg.owner, text.as_ref())
206: .with_parse_mode(ParseMode::Html),
207: MyMessage::HtmlTo { text, to } =>
208: SendMessage::new(*to, text.as_ref())
209: .with_parse_mode(ParseMode::Html),
210: MyMessage::HtmlToKb { text, to, kb } =>
211: SendMessage::new(*to, text.as_ref())
212: .with_parse_mode(ParseMode::Html)
213: .with_reply_markup(kb.clone()),
be0b8602d1 2026-04-18 214: })
215: }
216: }
217:
218: #[derive(Clone)]
219: pub struct Tg {
220: pub me: Bot,
221: pub owner: ChatPeerId,
222: pub client: Client,
223: }
224:
225: impl Tg {
226: /// Construct a new `Tg` instance from configuration.
227: ///
228: /// The `settings` must provide the following keys:
229: /// - `"api_key"` (string),
230: /// - `"owner"` (integer chat id),
231: /// - `"api_gateway"` (string).
232: ///
233: /// The function initialises the client, configures the gateway and fetches the bot identity
234: /// before returning the constructed `Tg`.
235: pub async fn new (settings: &config::Config) -> Result<Tg> {
236: let api_key = settings.get_string("api_key").stack()?;
237:
238: let owner = ChatPeerId::from(settings.get_int("owner").stack()?);
239: // We don't use retries, as in async environment this will just get us stuck for extra
240: // amount of time on simple requests. Just bail, show error and ack it in the code. In
241: // other case we might got stuck with multiple open transactions in database.
242: let client = Client::new(&api_key).stack()?
243: .with_host(settings.get_string("api_gateway").stack()?)
244: .with_max_retries(0);
245: let me = client.execute(GetBot).await.stack()?;
246: Ok(Tg {
247: me,
248: owner,
249: client,
250: })
251: }
252:
253: /// Send a text message to a chat, using an optional target and parse mode.
254: ///
255: /// # Returns
256: /// The sent `Message` on success.
257: pub async fn send (&self, msg: MyMessage<'_>) -> Result<Message> {
be0b8602d1 2026-04-18 258: self.client.execute(msg.req(self)?).await.stack()
259: }
260:
261: pub async fn answer_cb (&self, id: String, text: String) -> Result<bool> {
262: self.client.execute(
263: AnswerCallbackQuery::new(id)
264: .with_text(text)
265: ).await.stack()
266: }
267:
268: /// Create a copy of this `Tg` with the owner replaced by the given chat ID.
269: ///
270: /// # Parameters
271: /// - `owner`: The Telegram chat identifier to set as the new owner (expressed as an `i64`).
272: ///
273: /// # Returns
274: /// A new `Tg` instance identical to the original except its `owner` field is set to the provided chat ID.
275: pub fn with_owner <O>(&self, owner: O) -> Tg
276: where O: Into<i64> {
277: Tg {
278: owner: ChatPeerId::from(owner.into()),
279: ..self.clone()
280: }
281: }
282:
283: pub async fn update_message (&self, chat_id: i64, message_id: i64, text: String, feeds: &Arc<Mutex<FeedList>>, cb: Callback) -> Result<EditMessageResult> {
284: loop {
285: let req = EditMessageText::for_chat_message(chat_id, message_id, &text)
286: .with_reply_markup(get_kb(&cb, feeds).await.stack()?);
287: let res = self.client.execute(req).await;
288: match res {
289: Ok(res) => return Ok(res),
290: Err(ref err) => {
291: if let ExecuteError::Response(resp) = err
292: && let Some(delay) = resp.retry_after()
293: {
294: if delay > 60 {
295: return res.context("Delay too big (>60), not waiting.");
296: }
297: Timer::after(Duration::from_secs(delay)).await;
298: } else {
299: return res.context("Can't update message");
300: }
301: },
302: };
303: }
304: }
305: }