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