9adc69d514 2026-03-25 1: use crate::{
9adc69d514 2026-03-25 2: Arc,
9adc69d514 2026-03-25 3: Mutex,
9adc69d514 2026-03-25 4: };
9adc69d514 2026-03-25 5:
9adc69d514 2026-03-25 6: use std::{
9adc69d514 2026-03-25 7: borrow::Cow,
9adc69d514 2026-03-25 8: collections::HashMap,
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::{
9c4f09193a 2026-01-09 24: Bot,
9c4f09193a 2026-01-09 25: ChatPeerId,
9c4f09193a 2026-01-09 26: GetBot,
13265e7697 2026-01-10 27: InlineKeyboardButton,
13265e7697 2026-01-10 28: InlineKeyboardMarkup,
9c4f09193a 2026-01-09 29: Message,
9c4f09193a 2026-01-09 30: ParseMode,
9c4f09193a 2026-01-09 31: SendMessage,
9c4f09193a 2026-01-09 32: },
9c4f09193a 2026-01-09 33: };
13265e7697 2026-01-10 34:
9adc69d514 2026-03-25 35: const CB_VERSION: u8 = 0;
9adc69d514 2026-03-25 36:
13265e7697 2026-01-10 37: #[derive(Serialize, Deserialize, Debug)]
9adc69d514 2026-03-25 38: pub enum Callback {
9adc69d514 2026-03-25 39: // List all feeds (version, name to show, page number)
9adc69d514 2026-03-25 40: List(u8, String, u8),
9adc69d514 2026-03-25 41: }
9adc69d514 2026-03-25 42:
9adc69d514 2026-03-25 43: impl Callback {
9adc69d514 2026-03-25 44: pub fn list (text: &str, page: u8) -> Callback {
9adc69d514 2026-03-25 45: Callback::List(CB_VERSION, text.to_owned(), page)
9adc69d514 2026-03-25 46: }
9adc69d514 2026-03-25 47:
9adc69d514 2026-03-25 48: fn version (&self) -> u8 {
9adc69d514 2026-03-25 49: match self {
9adc69d514 2026-03-25 50: Callback::List(version, .. ) => *version,
9adc69d514 2026-03-25 51: }
9adc69d514 2026-03-25 52: }
9adc69d514 2026-03-25 53: }
9adc69d514 2026-03-25 54:
9adc69d514 2026-03-25 55: impl fmt::Display for Callback {
9adc69d514 2026-03-25 56: fn fmt (&self, f: &mut fmt::Formatter) -> fmt::Result {
9adc69d514 2026-03-25 57: f.write_str(&toml::to_string(self).map_err(|_| fmt::Error)?)
9adc69d514 2026-03-25 58: }
9adc69d514 2026-03-25 59: }
9adc69d514 2026-03-25 60:
9adc69d514 2026-03-25 61: /// Produce new Keyboard Markup from current Callback
9adc69d514 2026-03-25 62: pub async fn get_kb (cb: &Callback, feeds: Arc<Mutex<HashMap<i32, String>>>) -> Result<InlineKeyboardMarkup> {
9adc69d514 2026-03-25 63: if cb.version() != CB_VERSION {
9adc69d514 2026-03-25 64: bail!("Wrong callback version.");
9adc69d514 2026-03-25 65: }
9adc69d514 2026-03-25 66: let mark = match cb {
9adc69d514 2026-03-25 67: Callback::List(_, name, page) => {
9adc69d514 2026-03-25 68: let mut kb = vec![];
9adc69d514 2026-03-25 69: let feeds = feeds.lock_arc().await;
9adc69d514 2026-03-25 70: let long = feeds.len() > 6;
9adc69d514 2026-03-25 71: let (start, end) = if long {
9adc69d514 2026-03-25 72: (page * 5, 5 + page * 5)
9adc69d514 2026-03-25 73: } else {
9adc69d514 2026-03-25 74: (0, 6)
9adc69d514 2026-03-25 75: };
9adc69d514 2026-03-25 76: let mut i = 0;
9adc69d514 2026-03-25 77: for (id, name) in feeds.iter() {
9adc69d514 2026-03-25 78: if i < start { continue }
9adc69d514 2026-03-25 79: if i > end { break }
9adc69d514 2026-03-25 80: i += 1;
9adc69d514 2026-03-25 81: kb.push(vec![
9adc69d514 2026-03-25 82: InlineKeyboardButton::for_callback_data(
9adc69d514 2026-03-25 83: format!("{}. {}", id, name),
9adc69d514 2026-03-25 84: Callback::list("xxx", *page).to_string()), // XXX edit
9adc69d514 2026-03-25 85: ])
9adc69d514 2026-03-25 86: }
9adc69d514 2026-03-25 87: if name.is_empty() {
9adc69d514 2026-03-25 88: // no name - reverting to pages, any unknown number means last page
9adc69d514 2026-03-25 89: kb.push(vec![
9adc69d514 2026-03-25 90: InlineKeyboardButton::for_callback_data("1",
9adc69d514 2026-03-25 91: Callback::list("xxx", 0).to_string()),
9adc69d514 2026-03-25 92: ])
9adc69d514 2026-03-25 93: } else {
9adc69d514 2026-03-25 94: kb.push(vec![
9adc69d514 2026-03-25 95: InlineKeyboardButton::for_callback_data("1",
9adc69d514 2026-03-25 96: Callback::list("xxx", 0).to_string()),
9adc69d514 2026-03-25 97: ])
9adc69d514 2026-03-25 98: }
9adc69d514 2026-03-25 99: if long {
9adc69d514 2026-03-25 100: kb.push(vec![
9adc69d514 2026-03-25 101: InlineKeyboardButton::for_callback_data("<<",
9adc69d514 2026-03-25 102: Callback::list("", if *page == 0 { *page } else { page - 1 } ).to_string()),
9adc69d514 2026-03-25 103: InlineKeyboardButton::for_callback_data(">>",
9adc69d514 2026-03-25 104: Callback::list("", page + 1).to_string()),
9adc69d514 2026-03-25 105: ]);
9adc69d514 2026-03-25 106: }
9adc69d514 2026-03-25 107: InlineKeyboardMarkup::from(kb)
9adc69d514 2026-03-25 108: },
9adc69d514 2026-03-25 109: };
9adc69d514 2026-03-25 110: Ok(mark)
9adc69d514 2026-03-25 111: }
9adc69d514 2026-03-25 112:
9adc69d514 2026-03-25 113: pub enum MyMessage <'a> {
9adc69d514 2026-03-25 114: Html { text: Cow<'a, str> },
9adc69d514 2026-03-25 115: HtmlTo { text: Cow<'a, str>, to: ChatPeerId },
9adc69d514 2026-03-25 116: HtmlToKb { text: Cow<'a, str>, to: ChatPeerId, kb: InlineKeyboardMarkup },
9adc69d514 2026-03-25 117: Text { text: Cow<'a, str> },
9adc69d514 2026-03-25 118: TextTo { text: Cow<'a, str>, to: ChatPeerId },
13265e7697 2026-01-10 119: }
13265e7697 2026-01-10 120:
9adc69d514 2026-03-25 121: impl MyMessage <'_> {
9adc69d514 2026-03-25 122: pub fn html <'a, S> (text: S) -> MyMessage<'a>
9adc69d514 2026-03-25 123: where S: Into<Cow<'a, str>> {
9adc69d514 2026-03-25 124: let text = text.into();
9adc69d514 2026-03-25 125: MyMessage::Html { text }
9adc69d514 2026-03-25 126: }
9adc69d514 2026-03-25 127:
9adc69d514 2026-03-25 128: pub fn html_to <'a, S> (text: S, to: ChatPeerId) -> MyMessage<'a>
9adc69d514 2026-03-25 129: where S: Into<Cow<'a, str>> {
9adc69d514 2026-03-25 130: let text = text.into();
9adc69d514 2026-03-25 131: MyMessage::HtmlTo { text, to }
9adc69d514 2026-03-25 132: }
9adc69d514 2026-03-25 133:
9adc69d514 2026-03-25 134: pub fn html_to_kb <'a, S> (text: S, to: ChatPeerId, kb: InlineKeyboardMarkup) -> MyMessage<'a>
9adc69d514 2026-03-25 135: where S: Into<Cow<'a, str>> {
9adc69d514 2026-03-25 136: let text = text.into();
9adc69d514 2026-03-25 137: MyMessage::HtmlToKb { text, to, kb }
9adc69d514 2026-03-25 138: }
9adc69d514 2026-03-25 139:
9adc69d514 2026-03-25 140: pub fn text <'a, S> (text: S) -> MyMessage<'a>
9adc69d514 2026-03-25 141: where S: Into<Cow<'a, str>> {
9adc69d514 2026-03-25 142: let text = text.into();
9adc69d514 2026-03-25 143: MyMessage::Text { text }
9adc69d514 2026-03-25 144: }
9adc69d514 2026-03-25 145:
9adc69d514 2026-03-25 146: pub fn text_to <'a, S> (text: S, to: ChatPeerId) -> MyMessage<'a>
9adc69d514 2026-03-25 147: where S: Into<Cow<'a, str>> {
9adc69d514 2026-03-25 148: let text = text.into();
9adc69d514 2026-03-25 149: MyMessage::TextTo { text, to }
9adc69d514 2026-03-25 150: }
9adc69d514 2026-03-25 151:
9adc69d514 2026-03-25 152: fn req (&self, tg: &Tg) -> Result<SendMessage> {
9adc69d514 2026-03-25 153: Ok(match self {
9adc69d514 2026-03-25 154: MyMessage::Html { text } =>
9adc69d514 2026-03-25 155: SendMessage::new(tg.owner, text.as_ref())
9adc69d514 2026-03-25 156: .with_parse_mode(ParseMode::Html),
9adc69d514 2026-03-25 157: MyMessage::HtmlTo { text, to } =>
9adc69d514 2026-03-25 158: SendMessage::new(*to, text.as_ref())
9adc69d514 2026-03-25 159: .with_parse_mode(ParseMode::Html),
9adc69d514 2026-03-25 160: MyMessage::HtmlToKb { text, to, kb } =>
9adc69d514 2026-03-25 161: SendMessage::new(*to, text.as_ref())
9adc69d514 2026-03-25 162: .with_parse_mode(ParseMode::Html)
9adc69d514 2026-03-25 163: .with_reply_markup(kb.clone()),
9adc69d514 2026-03-25 164: MyMessage::Text { text } =>
9adc69d514 2026-03-25 165: SendMessage::new(tg.owner, text.as_ref())
9adc69d514 2026-03-25 166: .with_parse_mode(ParseMode::MarkdownV2),
9adc69d514 2026-03-25 167: MyMessage::TextTo { text, to } =>
9adc69d514 2026-03-25 168: SendMessage::new(*to, text.as_ref())
9adc69d514 2026-03-25 169: .with_parse_mode(ParseMode::MarkdownV2),
9adc69d514 2026-03-25 170: })
9adc69d514 2026-03-25 171: }
13265e7697 2026-01-10 172: }
9c4f09193a 2026-01-09 173:
9c4f09193a 2026-01-09 174: #[derive(Clone)]
9c4f09193a 2026-01-09 175: pub struct Tg {
9c4f09193a 2026-01-09 176: pub me: Bot,
9c4f09193a 2026-01-09 177: pub owner: ChatPeerId,
9c4f09193a 2026-01-09 178: pub client: Client,
9c4f09193a 2026-01-09 179: }
9c4f09193a 2026-01-09 180:
9c4f09193a 2026-01-09 181: impl Tg {
fabcca1eaf 2026-01-09 182: /// Construct a new `Tg` instance from configuration.
fabcca1eaf 2026-01-09 183: ///
fabcca1eaf 2026-01-09 184: /// The `settings` must provide the following keys:
fabcca1eaf 2026-01-09 185: /// - `"api_key"` (string),
fabcca1eaf 2026-01-09 186: /// - `"owner"` (integer chat id),
fabcca1eaf 2026-01-09 187: /// - `"api_gateway"` (string).
fabcca1eaf 2026-01-09 188: ///
fabcca1eaf 2026-01-09 189: /// The function initialises the client, configures the gateway and fetches the bot identity
fabcca1eaf 2026-01-09 190: /// before returning the constructed `Tg`.
9c4f09193a 2026-01-09 191: pub async fn new (settings: &config::Config) -> Result<Tg> {
9c4f09193a 2026-01-09 192: let api_key = settings.get_string("api_key").stack()?;
9c4f09193a 2026-01-09 193:
9c4f09193a 2026-01-09 194: let owner = ChatPeerId::from(settings.get_int("owner").stack()?);
9c4f09193a 2026-01-09 195: let client = Client::new(&api_key).stack()?
9c4f09193a 2026-01-09 196: .with_host(settings.get_string("api_gateway").stack()?)
9c4f09193a 2026-01-09 197: .with_max_retries(0);
9c4f09193a 2026-01-09 198: let me = client.execute(GetBot).await.stack()?;
9c4f09193a 2026-01-09 199: Ok(Tg {
9c4f09193a 2026-01-09 200: me,
9c4f09193a 2026-01-09 201: owner,
9c4f09193a 2026-01-09 202: client,
9c4f09193a 2026-01-09 203: })
9c4f09193a 2026-01-09 204: }
9c4f09193a 2026-01-09 205:
fabcca1eaf 2026-01-09 206: /// Send a text message to a chat, using an optional target and parse mode.
fabcca1eaf 2026-01-09 207: ///
fabcca1eaf 2026-01-09 208: /// # Returns
fabcca1eaf 2026-01-09 209: /// The sent `Message` on success.
9adc69d514 2026-03-25 210: pub async fn send (&self, msg: MyMessage<'_>) -> Result<Message> {
9adc69d514 2026-03-25 211: self.client.execute(msg.req(self)?).await.stack()
fabcca1eaf 2026-01-09 212: }
fabcca1eaf 2026-01-09 213:
fabcca1eaf 2026-01-09 214: /// Create a copy of this `Tg` with the owner replaced by the given chat ID.
fabcca1eaf 2026-01-09 215: ///
fabcca1eaf 2026-01-09 216: /// # Parameters
fabcca1eaf 2026-01-09 217: /// - `owner`: The Telegram chat identifier to set as the new owner (expressed as an `i64`).
fabcca1eaf 2026-01-09 218: ///
fabcca1eaf 2026-01-09 219: /// # Returns
fabcca1eaf 2026-01-09 220: /// A new `Tg` instance identical to the original except its `owner` field is set to the provided chat ID.
fabcca1eaf 2026-01-09 221: pub fn with_owner <O>(&self, owner: O) -> Tg
fabcca1eaf 2026-01-09 222: where O: Into<i64> {
fabcca1eaf 2026-01-09 223: Tg {
fabcca1eaf 2026-01-09 224: owner: ChatPeerId::from(owner.into()),
9c4f09193a 2026-01-09 225: ..self.clone()
9c4f09193a 2026-01-09 226: }
9c4f09193a 2026-01-09 227: }
9c4f09193a 2026-01-09 228: }