tg_bot.rs
Logged in as anonymous

File src/tg_bot.rs from the latest check-in


use crate::{
	Arc,
	Mutex,
	core::FeedList,
};

use std::{
	borrow::Cow,
	fmt,
	time::Duration,
};

use serde::{
	Deserialize,
	Serialize,
};
use smol::Timer;
use stacked_errors::{
	bail,
	Result,
	StackableErr,
};
use tgbot::{
	api::{
		Client,
		ExecuteError
	},
	types::{
		AnswerCallbackQuery,
		Bot,
		ChatPeerId,
		EditMessageResult,
		EditMessageText,
		GetBot,
		InlineKeyboardButton,
		InlineKeyboardMarkup,
		Message,
		ParseMode,
		SendMessage,
	},
};

const CB_VERSION: u8 = 0;

#[derive(Serialize, Deserialize, Debug)]
pub enum Callback {
	// Edit one feed (version, name)
	Edit(u8, String),
	// List all feeds (version, name to show, page number)
	List(u8, String, usize),
	// Show root menu (version)
	Menu(u8),
}

impl Callback {
	pub fn edit <S>(text: S) -> Callback
	where S: Into<String> {
		Callback::Edit(CB_VERSION, text.into())
	}

	pub fn list <S>(text: S, page: usize) -> Callback
	where S: Into<String> {
		Callback::List(CB_VERSION, text.into(), page)
	}

	pub fn menu () -> Callback {
		Callback::Menu(CB_VERSION)
	}

	fn version (&self) -> u8 {
		match self {
			Callback::Edit(version, .. ) => *version,
			Callback::List(version, .. ) => *version,
			Callback::Menu(version) => *version,
		}
	}
}

impl fmt::Display for Callback {
	fn fmt (&self, f: &mut fmt::Formatter) -> fmt::Result {
		f.write_str(&toml::to_string(self).map_err(|_| fmt::Error)?)
	}
}

/// Produce new Keyboard Markup from current Callback
pub async fn get_kb (cb: &Callback, feeds: &Arc<Mutex<FeedList>>) -> Result<InlineKeyboardMarkup> {
	if cb.version() != CB_VERSION {
		bail!("Wrong callback version.");
	}
	let mark = match cb {
		Callback::Edit(_, _name) => { // XXX edit missing
			let kb: Vec<Vec<InlineKeyboardButton>> = vec![];
			InlineKeyboardMarkup::from(kb)
		},
		Callback::List(_, name, page) => {
			let mut kb = vec![];
			let feeds = feeds.lock_arc().await;
			let long = feeds.len() > 6;
			let (start, end) = if long {
				(page * 5, 5 + page * 5)
			} else {
				(0, 6)
			};
			let mut i = 0;
			if name.is_empty() {
				let feed_iter = feeds.iter().skip(start);
				for (id, name) in feed_iter {
					kb.push(vec![
						InlineKeyboardButton::for_callback_data(
							format!("{}. {}", id, name),
							Callback::edit(name).to_string()),
					]);
					i += 1;
					if i == end { break }
				}
			} else {
				let mut found = false;
				let mut first_page = None;
				for (id, feed_name) in feeds.iter() {
					if name == feed_name {
						found = true;
					}
					i += 1;
					kb.push(vec![
						InlineKeyboardButton::for_callback_data(
							format!("{}. {}", id, feed_name),
							Callback::list("xxx", *page).to_string()), // XXX edit
					]);
					if i == end {
						// page complete, if found we got the right page, if not - reset and
						// continue.
						if found {
							break
						} else {
							if first_page.is_none() {
								first_page = Some(kb);
							}
							kb = vec![];
							i = 0
						}
					}
				}
				if !found {
					// name not found, showing first page
					kb = first_page.unwrap_or_default();
				}
			}
			if long {
				kb.push(vec![
					InlineKeyboardButton::for_callback_data("<<",
						Callback::list("", if *page == 0 { *page } else { page - 1 } ).to_string()),
					InlineKeyboardButton::for_callback_data(">>",
						Callback::list("", page + 1).to_string()),
				]);
			}
			InlineKeyboardMarkup::from(kb)
		},
		Callback::Menu(_) => {
			let kb = vec![
				vec![
					InlineKeyboardButton::for_callback_data(
						"Add new channel",
						Callback::menu().to_string()), // new XXX
				],
				vec![
					InlineKeyboardButton::for_callback_data(
						"List channels",
						Callback::list("", 0).to_string()),
				],
			];
			InlineKeyboardMarkup::from(kb)
		},
	};
	Ok(mark)
}

pub enum MyMessage <'a> {
	Html { text: Cow<'a, str> },
	HtmlTo { text: Cow<'a, str>, to: ChatPeerId },
	HtmlToKb { text: Cow<'a, str>, to: ChatPeerId, kb: InlineKeyboardMarkup },
}

impl MyMessage <'_> {
	pub fn html <'a, S> (text: S) -> MyMessage<'a>
	where S: Into<Cow<'a, str>> {
		let text = text.into();
		MyMessage::Html { text }
	}
	
	pub fn html_to <'a, S> (text: S, to: ChatPeerId) -> MyMessage<'a>
	where S: Into<Cow<'a, str>> {
		let text = text.into();
		MyMessage::HtmlTo { text, to }
	}
	
	pub fn html_to_kb <'a, S> (text: S, to: ChatPeerId, kb: InlineKeyboardMarkup) -> MyMessage<'a>
	where S: Into<Cow<'a, str>> {
		let text = text.into();
		MyMessage::HtmlToKb { text, to, kb }
	}
	
	fn req (&self, tg: &Tg) -> Result<SendMessage> {
		Ok(match self {
			MyMessage::Html { text } =>
				SendMessage::new(tg.owner, text.as_ref())
					.with_parse_mode(ParseMode::Html),
			MyMessage::HtmlTo { text, to } =>
				SendMessage::new(*to, text.as_ref())
					.with_parse_mode(ParseMode::Html),
			MyMessage::HtmlToKb { text, to, kb } =>
				SendMessage::new(*to, text.as_ref())
					.with_parse_mode(ParseMode::Html)
					.with_reply_markup(kb.clone()),
		})
	}
}

#[derive(Clone)]
pub struct Tg {
	pub me: Bot,
	pub owner: ChatPeerId,
	pub client: Client,
}

impl Tg {
	/// Construct a new `Tg` instance from configuration.
	///
	/// The `settings` must provide the following keys:
	///  - `"api_key"` (string),
	///  - `"owner"` (integer chat id),
	///  - `"api_gateway"` (string).
	///
	/// The function initialises the client, configures the gateway and fetches the bot identity
	/// before returning the constructed `Tg`.
	pub async fn new (settings: &config::Config) -> Result<Tg> {
		let api_key = settings.get_string("api_key").stack()?;

		let owner = ChatPeerId::from(settings.get_int("owner").stack()?);
		// We don't use retries, as in async environment this will just get us stuck for extra
		// amount of time on simple requests. Just bail, show error and ack it in the code. In
		// other case we might got stuck with multiple open transactions in database.
		let client = Client::new(&api_key).stack()?
			.with_host(settings.get_string("api_gateway").stack()?)
			.with_max_retries(0);
		let me = client.execute(GetBot).await.stack()?;
		Ok(Tg {
			me,
			owner,
			client,
		})
	}

	/// Send a text message to a chat, using an optional target and parse mode.
	///
	/// # Returns
	/// The sent `Message` on success.
	pub async fn send (&self, msg: MyMessage<'_>) -> Result<Message> {
		self.client.execute(msg.req(self)?).await.stack()
	}

	pub async fn answer_cb (&self, id: String, text: String) -> Result<bool> {
		self.client.execute(
			AnswerCallbackQuery::new(id)
				.with_text(text)
		).await.stack()
	}

	/// Create a copy of this `Tg` with the owner replaced by the given chat ID.
	///
	/// # Parameters
	/// - `owner`: The Telegram chat identifier to set as the new owner (expressed as an `i64`).
	///
	/// # Returns
	/// A new `Tg` instance identical to the original except its `owner` field is set to the provided chat ID.
	pub fn with_owner <O>(&self, owner: O) -> Tg
	where O: Into<i64> {
		Tg {
			owner: ChatPeerId::from(owner.into()),
			..self.clone()
		}
	}

	pub async fn update_message (&self, chat_id: i64, message_id: i64, text: String, feeds: &Arc<Mutex<FeedList>>, cb: Callback) -> Result<EditMessageResult> {
		loop {
			let req = EditMessageText::for_chat_message(chat_id, message_id, &text)
				.with_reply_markup(get_kb(&cb, feeds).await.stack()?);
			let res = self.client.execute(req).await;
			match res {
				Ok(res) => return Ok(res),
				Err(ref err) => {
					if let ExecuteError::Response(resp) = err
						&& let Some(delay) = resp.retry_after()
					{
						if delay > 60 {
							return res.context("Delay too big (>60), not waiting.");
						}
						Timer::after(Duration::from_secs(delay)).await;
					} else {
						return res.context("Can't update message");
					}
				},
			};
		}
	}
}