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