Annotation For src/tg_bot.rs
Logged in as anonymous

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: }