deadman 0.1.0

A deadman switch implemantation that will notify on Telegram if the user has not interacted with it in a specific interval
Documentation
use chrono::DateTime;
use sqlx::SqlitePool;
use teloxide::{
    dispatching::{dialogue, UpdateFilterExt, UpdateHandler},
    dptree,
    prelude::{Dialogue, Dispatcher},
    requests::Requester,
    types::{ChatId, Message, Recipient, Update},
    utils::command::BotCommands,
    Bot,
};

use crate::config::NOTIFY_DELAY_MINUTES;

pub async fn maybe_notify(bot: Bot, pool: SqlitePool) -> anyhow::Result<()> {
    tracing::debug!("Maybe notify");
    let users = sqlx::query!(
        r#" 
SELECT
	user_id,
    name,
	telegram_id,
    last_call
FROM
	user
WHERE
	(last_call + interval_minutes * 60 ) < CAST(strftime('%s', 'now') as INTEGER)
    AND last_notification IS NULL
    "#
    )
    .fetch_all(&pool)
    .await?;
    tracing::info!("Users {:?} should be notified", users);
    for user in users {
        let _ = bot
            .send_message(
                Recipient::Id(ChatId(user.telegram_id)),
                format!(
                    "Check if {} is still alive. Last message was sent at {}",
                    user.name,
                    DateTime::from_timestamp(user.last_call, 0)
                        .unwrap()
                        .to_rfc3339()
                ),
            )
            .await?;
        let _ = sqlx::query!(
            r#" 
            UPDATE user
            SET last_notification = CAST(strftime('%s', 'now') as INTEGER)
            WHERE user_id = $1
        "#,
            user.user_id
        )
        .execute(&pool)
        .await?;
    }
    Ok(())
}

#[derive(Clone, Default)]
pub enum State {
    #[default]
    Start,
    ReceiveFullName,
}

#[derive(BotCommands, Clone)]
#[command(
    rename_rule = "lowercase",
    description = "These commands are supported:"
)]
enum Command {
    #[command(description = "display this text.")]
    Help,
    #[command(description = "start the purchase procedure.")]
    Start,
    #[command(description = "cancel the purchase procedure.")]
    Cancel,
}

fn schema() -> UpdateHandler<Box<dyn std::error::Error + Send + Sync + 'static>> {
    use dptree::case;

    let command_handler = teloxide::filter_command::<Command, _>()
        .branch(
            case![State::Start]
                .branch(case![Command::Help].endpoint(help))
                .branch(case![Command::Start].endpoint(start)),
        )
        .branch(case![Command::Cancel].endpoint(cancel));

    let message_handler = Update::filter_message()
        .branch(command_handler)
        .branch(case![State::ReceiveFullName].endpoint(receive_full_name))
        .branch(dptree::endpoint(invalid_state));

    dialogue::enter::<Update, dialogue::InMemStorage<State>, State, _>().branch(message_handler)
}

pub async fn start_bot(bot: Bot, pool: SqlitePool) -> anyhow::Result<()> {
    tokio::spawn(async move {
        Dispatcher::builder(bot, schema())
            .dependencies(dptree::deps![dialogue::InMemStorage::<State>::new(), pool])
            .enable_ctrlc_handler()
            .build()
            .dispatch()
            .await;
    });
    Ok(())
}

type MyDialogue = Dialogue<State, dialogue::InMemStorage<State>>;
type HandlerResult = Result<(), Box<dyn std::error::Error + Send + Sync>>;

async fn start(bot: Bot, dialogue: MyDialogue, msg: Message) -> HandlerResult {
    bot.send_message(msg.chat.id, "Let's start! What's your full name?")
        .await?;
    dialogue.update(State::ReceiveFullName).await?;
    Ok(())
}
async fn help(bot: Bot, msg: Message) -> HandlerResult {
    bot.send_message(msg.chat.id, Command::descriptions().to_string())
        .await?;
    Ok(())
}
async fn cancel(bot: Bot, dialogue: MyDialogue, msg: Message) -> HandlerResult {
    bot.send_message(msg.chat.id, "Cancelling the dialogue.")
        .await?;
    dialogue.exit().await?;
    Ok(())
}
async fn invalid_state(bot: Bot, msg: Message) -> HandlerResult {
    bot.send_message(
        msg.chat.id,
        "Unable to handle the message. Type /help to see the usage.",
    )
    .await?;
    Ok(())
}
async fn receive_full_name(
    bot: Bot,
    dialogue: MyDialogue,
    msg: Message,
    pool: SqlitePool,
) -> HandlerResult {
    match msg.text().map(ToOwned::to_owned) {
        Some(name) => {
            let id = uuid::Uuid::new_v4();
            let id = id.hyphenated().to_string();
            let now = chrono::Utc::now().timestamp();
            let res = sqlx::query!(
                r#"INSERT INTO user
                (user_id, name, telegram_id, last_call, interval_minutes) 
                VALUES
                ($1, $2, $3, $4, $5)"#,
                id,
                name,
                msg.chat.id.0,
                now,
                NOTIFY_DELAY_MINUTES,
            )
            .execute(&pool)
            .await;
            match res {
                Ok(_) => {
                    bot.send_message(msg.chat.id, format!("You are registered with id '{}'", id))
                        .await?;
                }
                Err(e) => {
                    tracing::error!("Sql issue {:?}", e);
                    bot.send_message(msg.chat.id, format!("sql issue")).await?;
                }
            }

            dialogue.exit().await?;
        }
        None => {
            bot.send_message(msg.chat.id, "Please, send me your name.")
                .await?;
        }
    }
    Ok(())
}