use std::sync::{Arc, RwLock};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use teloxide::{
dispatching::{
UpdateHandler,
dialogue::{self, InMemStorage},
},
prelude::*,
types::{CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup, Message, MessageId},
utils::command::BotCommands,
};
use tracing::info;
use v_utils::prelude::*;
use crate::{
config::LiveSettings,
data::{Data, ValuePath},
utils::{get_json_type, value_preview},
};
type MyDialogue = Dialogue<ChatState, InMemStorage<ChatState>>;
type HandlerResult = Result<(), Box<dyn std::error::Error + Send + Sync>>;
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
enum ChatState {
#[default]
Unauthorized,
Authorized,
Navigation {
message_id: i32,
},
Input(ValueInput),
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, derive_new::new)]
struct ValueInput {
input_type: InputValueType,
value_path: ValuePath,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub enum InputValueType {
UpdateAt,
AddTo,
RemoveFrom,
}
#[derive(BotCommands, Clone, Debug)]
#[command(description = "Commands:", rename_rule = "lowercase")]
enum Command {
#[command(description = "Display all commands")]
Help,
#[command(description = "Open admin panel at the top")]
Admin,
}
#[tracing::instrument]
pub async fn run(settings: Arc<LiveSettings>, data: Arc<RwLock<Data>>) -> Result<()> {
let token = settings.config()?.tg_token;
let bot = Bot::new(token);
info!("Starting telegram bot...");
Dispatcher::builder(bot, schema())
.dependencies(dptree::deps![data, settings, InMemStorage::<ChatState>::new()])
.error_handler(LoggingErrorHandler::with_custom_text("An error has occurred in the dispatcher"))
.enable_ctrlc_handler()
.build()
.dispatch()
.await;
Ok(())
}
fn schema() -> UpdateHandler<Box<dyn std::error::Error + Send + Sync + 'static>> {
use dptree::case;
let command_handler = teloxide::filter_command::<Command, _>()
.branch(case![Command::Help].endpoint(help_handler))
.branch(case![Command::Admin].endpoint(admin_handler));
let message_handler = Update::filter_message()
.branch(command_handler)
.branch(case![ChatState::Input(value_input)].endpoint(value_input_handler))
.branch(dptree::endpoint(invalid_state_handler));
let callback_query_handler = Update::filter_callback_query().endpoint(callback_query_handler);
let auth_handler = dptree::filter_map_async(|dialogue: MyDialogue, settings: Arc<LiveSettings>, update: Update| async move {
match dialogue.get().await {
Ok(Some(ChatState::Unauthorized)) => {
if let Some(admin_list) = &settings.config().ok()?.admin_list {
let user_id = update.from()?.id.0;
if !admin_list.contains(&user_id) {
return None; }
}
dialogue.update(ChatState::Authorized).await.ok()?;
Some(()) }
Ok(Some(_)) => Some(()), _ => None, }
});
dialogue::enter::<Update, InMemStorage<ChatState>, ChatState, _>()
.chain(auth_handler)
.branch(message_handler)
.branch(callback_query_handler)
}
async fn admin_handler(bot: Bot, msg: Message, dialogue: MyDialogue, data: Arc<RwLock<Data>>) -> HandlerResult {
let value_path = ValuePath::default();
let (header, markup) = {
let data = data.read().unwrap();
render_header_and_markup(&data, &value_path)
};
let sent_message = bot.send_message(msg.chat.id, &header).reply_markup(markup).await?;
dialogue.update(ChatState::Navigation { message_id: sent_message.id.0 }).await?;
Ok(())
}
async fn value_input_handler(bot: Bot, dialogue: MyDialogue, msg: Message, value_input: ValueInput, data: Arc<RwLock<Data>>) -> HandlerResult {
match msg.text().map(ToOwned::to_owned) {
Some(new_value) => {
if let Ok(new_value) = serde_json::from_str::<Value>(&new_value) {
let update_result = {
let mut data_lock = data.write().unwrap();
let result = data_lock.update_at(&value_input.value_path, new_value.clone(), value_input.input_type);
data_lock.write().unwrap();
result
};
match update_result {
Ok(_) => {
let affirmation_menu = match value_input.input_type {
InputValueType::UpdateAt => {
format!("Value of `{}` has been updated to `{}`", &value_input.value_path, &new_value.to_string())
}
InputValueType::AddTo => {
format!("`{}` has been added to `{}`", &new_value.to_string(), &value_input.value_path)
}
InputValueType::RemoveFrom => {
format!("`{}` has been removed from `{}`", &new_value.to_string(), &value_input.value_path)
}
};
bot.send_message(msg.chat.id, affirmation_menu).await?;
let (header, markup) = {
let data = data.read().unwrap();
let new_path = match value_input.input_type {
InputValueType::UpdateAt => value_input.value_path.parent(),
InputValueType::AddTo | InputValueType::RemoveFrom => value_input.value_path,
};
render_header_and_markup(&data, &new_path)
};
let sent_message = bot.send_message(dialogue.chat_id(), &header).reply_markup(markup).await?;
dialogue.update(ChatState::Navigation { message_id: sent_message.id.0 }).await?;
}
Err(e) => {
bot.send_message(msg.chat.id, e).await?;
}
}
} else {
bot.send_message(msg.chat.id, "Invalid value. Input valid JSON value.").await?;
}
}
None => {
bot.send_message(msg.chat.id, "Please send the new value.").await?;
}
}
Ok(())
}
async fn invalid_state_handler(bot: Bot, msg: Message) -> HandlerResult {
bot.send_message(msg.chat.id, "Unable to handle the message. Type /help to see available commands.").await?;
Ok(())
}
async fn help_handler(bot: Bot, msg: Message) -> HandlerResult {
bot.send_message(msg.chat.id, Command::descriptions().to_string()).await?;
Ok(())
}
async fn callback_query_handler(bot: Bot, dialogue: MyDialogue, q: CallbackQuery, data: Arc<RwLock<Data>>) -> HandlerResult {
bot.answer_callback_query(q.id.clone()).await?; if let Some(j) = q.data {
let action: CallbackAction = serde_json::from_str(&j).unwrap();
match action {
CallbackAction::Go(value_path) => {
continue_navigation(bot.clone(), dialogue, data, value_path).await?;
}
CallbackAction::UpdateAt(value_path) => {
dialogue.update(ChatState::Input(ValueInput::new(InputValueType::UpdateAt, value_path.clone()))).await?;
bot.send_message(
dialogue.chat_id(),
format!("You're updating `{}: {}`.\n Insert the new value.", &value_path.basename(), {
let data_lock = data.read().unwrap();
get_json_type(&data_lock.at(&value_path).unwrap())
}),
)
.await?;
}
CallbackAction::AddTo(value_path) => {
dialogue.update(ChatState::Input(ValueInput::new(InputValueType::AddTo, value_path.clone()))).await?;
bot.send_message(dialogue.chat_id(), format!("You're adding to {}.\n Provide the value to add.", value_path))
.await?;
}
CallbackAction::RemoveFrom(value_path) => {
dialogue.update(ChatState::Input(ValueInput::new(InputValueType::RemoveFrom, value_path.clone()))).await?;
bot.send_message(dialogue.chat_id(), format!("You're removing from {}.\n Provide exact value to remove.", value_path))
.await?;
}
}
}
Ok(())
}
async fn continue_navigation(bot: Bot, dialogue: MyDialogue, data: Arc<RwLock<Data>>, value_path: ValuePath) -> HandlerResult {
let (header, markup) = {
let data = data.read().unwrap();
render_header_and_markup(&data, &value_path)
};
let state = dialogue.get().await.unwrap().unwrap();
let message_id = match state {
ChatState::Navigation { message_id } => message_id,
_ => unreachable!(),
};
match bot.edit_message_text(dialogue.chat_id(), MessageId(message_id), &header).reply_markup(markup.clone()).await {
Ok(_) => Ok(()),
Err(err) => {
dbg!(err);
let sent_message = bot.send_message(dialogue.chat_id(), &header).reply_markup(markup).await?;
dialogue.update(ChatState::Navigation { message_id: sent_message.id.0 }).await?;
Ok(())
}
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, derive_new::new)]
enum CallbackAction {
Go(ValuePath),
UpdateAt(ValuePath),
AddTo(ValuePath),
RemoveFrom(ValuePath),
}
fn render_header_and_markup(data: &Data, value_path: &ValuePath) -> (String, InlineKeyboardMarkup) {
let mut keyboard = Vec::new();
let current_value_at_path = &data.at(value_path).unwrap();
let mut header = value_path.to_string();
if !value_path.is_top() {
let callback_action = CallbackAction::Go(value_path.parent());
let button = InlineKeyboardButton::callback("..", serde_json::to_string(&callback_action).unwrap());
keyboard.push(vec![button]);
}
match current_value_at_path {
Value::Object(map) =>
for (key, val) in map {
let (display_text, callback_data) = match val {
Value::Object(_) | Value::Array(_) => (value_preview(key, val), CallbackAction::Go(value_path.join(key))),
_ => (value_preview(key, val), CallbackAction::UpdateAt(value_path.join(key))),
};
let button = InlineKeyboardButton::callback(display_text, serde_json::to_string(&callback_data).unwrap());
keyboard.push(vec![button]);
},
Value::Array(arr) => {
header.push_str(&format!(" [{}]", arr.len()));
let start = arr.len().saturating_sub(25);
let mut array_str = "\n```json\n".to_owned();
for a in arr.iter().skip(start) {
array_str.push_str(&format!("{a}\n"));
}
array_str.push_str("```");
header += &array_str;
let bottom_row = vec![
InlineKeyboardButton::callback("Add", serde_json::to_string(&CallbackAction::AddTo(value_path.clone())).unwrap()),
InlineKeyboardButton::callback("Remove", serde_json::to_string(&CallbackAction::RemoveFrom(value_path.clone())).unwrap()),
];
keyboard.push(bottom_row);
}
_ => {
unreachable!();
}
}
(header, InlineKeyboardMarkup::new(keyboard))
}
#[cfg(test)]
mod tests {
use serde_json::json;
use super::*;
fn gen_data() -> (Data, ValuePath) {
let json_value = json!({
"name": "Alice",
"age": 25,
"address": {
"street": "456 Another St",
"city": "Elsewhere"
},
"emails": ["alice@example.com", "a@example.com"]
});
(Data::mock(json_value), ValuePath::default())
}
#[test]
fn test_top_value_path_representation() {
let (data, value_path) = gen_data();
let (_h, r) = render_header_and_markup(&data, &value_path);
insta::assert_json_snapshot!(
r,
@r###"
{
"inline_keyboard": [
[
{
"text": "{} address",
"callback_data": "{\"Go\":\"/address\"}"
}
],
[
{
"text": "age: 25",
"callback_data": "{\"UpdateAt\":\"/age\"}"
}
],
[
{
"text": "[2] emails",
"callback_data": "{\"Go\":\"/emails\"}"
}
],
[
{
"text": "name: \"Alice\"",
"callback_data": "{\"UpdateAt\":\"/name\"}"
}
]
]
}
"###
);
}
#[test]
fn test_nested_value_path_representation() {
let (data, mut value_path) = gen_data();
value_path.push("address");
let (_h, r) = render_header_and_markup(&data, &value_path);
insta::assert_json_snapshot!(
r,
@r###"
{
"inline_keyboard": [
[
{
"text": "..",
"callback_data": "{\"Go\":\"/\"}"
}
],
[
{
"text": "city: \"Elsewhere\"",
"callback_data": "{\"UpdateAt\":\"/address/city\"}"
}
],
[
{
"text": "street: \"456 Another St\"",
"callback_data": "{\"UpdateAt\":\"/address/street\"}"
}
]
]
}
"###
);
}
#[test]
fn test_array_value_path_representation() {
let (data, mut value_path) = gen_data();
value_path.push("emails");
let (h, r) = render_header_and_markup(&data, &value_path);
insta::assert_snapshot!(h, "Admin Menu",);
insta::assert_json_snapshot!(
r,
@r###"
{
"inline_keyboard": [
[
{
"text": "..",
"callback_data": "{\"Go\":\"/\"}"
}
],
[
{
"text": "Add",
"callback_data": "{\"AddTo\":\"/emails\"}"
},
{
"text": "Remove",
"callback_data": "{\"RemoveFrom\":\"/emails\"}"
}
]
]
}
"###
);
}
}