use std::time::Duration as StdDuration;
use time::Duration as TimeDuration;
use time::OffsetDateTime;
use cratefield_adapter_telegram::{InlineButton, OutgoingMessage, TelegramBot, TelegramError};
use cratefield_core::{Clock, Database, RandomBytes, RandomError};
use crate::store;
use crate::token::{Decision, action_token};
pub const MAX_ACTION_TTL: TimeDuration = TimeDuration::seconds(60 * 60);
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ActionRequest {
pub subject: String,
pub action: String,
pub text: String,
pub value_moving: bool,
pub ttl: StdDuration,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ActionPrompt {
pub action_id: String,
pub chat_id: i64,
pub expires_at: OffsetDateTime,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum ActionError {
NotLinked,
Random(RandomError),
Db(cratefield_core::DbError),
Telegram(TelegramError),
}
impl std::fmt::Display for ActionError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::NotLinked => f.write_str("the subject has no linked Telegram account"),
Self::Random(error) => write!(f, "entropy source failed: {error}"),
Self::Db(error) => write!(f, "a database statement failed: {error}"),
Self::Telegram(error) => write!(f, "telegram refused the prompt: {error}"),
}
}
}
impl std::error::Error for ActionError {}
pub async fn send_action_prompt(
db: &dyn Database,
clock: &dyn Clock,
random: &dyn RandomBytes,
bot: &dyn TelegramBot,
key: &[u8],
request: ActionRequest,
) -> Result<ActionPrompt, ActionError> {
let Some((telegram_user_id, chat_id)) = store::link_of_subject(db, &request.subject)
.await
.map_err(ActionError::Db)?
else {
return Err(ActionError::NotLinked);
};
let mut bytes = [0u8; 16];
random.fill(&mut bytes).map_err(ActionError::Random)?;
let action_id = crate::token::b64url(&bytes);
let now = clock.now();
let ttl = request
.ttl
.try_into()
.unwrap_or(MAX_ACTION_TTL)
.clamp(TimeDuration::seconds(1), MAX_ACTION_TTL);
let expires_at = now + ttl;
let exp_unix = u32::try_from(expires_at.unix_timestamp()).unwrap_or(0);
store::insert_action(
db,
&action_id,
&request.subject,
&request.action,
request.value_moving,
&store::stamp(expires_at),
&store::stamp(now),
)
.await
.map_err(ActionError::Db)?;
let approve = action_token(
key,
&action_id,
Decision::Approve,
exp_unix,
telegram_user_id,
);
let deny = action_token(key, &action_id, Decision::Deny, exp_unix, telegram_user_id);
let message = OutgoingMessage::new(chat_id, request.text.clone()).row(vec![
InlineButton::callback("Approve", approve).map_err(ActionError::Telegram)?,
InlineButton::callback("Deny", deny).map_err(ActionError::Telegram)?,
]);
bot.send_message(&message)
.await
.map_err(ActionError::Telegram)?;
Ok(ActionPrompt {
action_id,
chat_id,
expires_at,
})
}