Annotation For src/tg_bot.rs
Logged in as anonymous

Origin for each line in src/tg_bot.rs from check-in d8c1d259a2:

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