use cratefield_core::{Database, DbError, Row, Statement};
use sea_query::{Alias, Expr, OnConflict, Query};
const CODES: &str = "telegram_link_codes";
const LINKS: &str = "telegram_links";
const ACTIONS: &str = "telegram_actions";
fn iden(name: &str) -> Alias {
Alias::new(name)
}
pub(crate) fn stamp(at: time::OffsetDateTime) -> String {
at.replace_nanosecond(0)
.expect("truncation stays in range")
.format(&time::format_description::well_known::Rfc3339)
.unwrap_or_default()
}
pub(crate) async fn insert_code(
db: &dyn Database,
code_hash: &str,
subject: &str,
expires_at: &str,
) -> Result<u64, DbError> {
let mut insert = Query::insert();
insert
.into_table(iden(CODES))
.columns(["code_hash", "subject", "expires_at", "consumed_at"])
.values_panic([
code_hash.into(),
subject.into(),
expires_at.into(),
Option::<String>::None.into(),
]);
db.execute(&Statement::render(&insert)).await
}
pub(crate) async fn consume_code(
db: &dyn Database,
code_hash: &str,
now: &str,
) -> Result<u64, DbError> {
let mut update = Query::update();
update
.table(iden(CODES))
.value(iden("consumed_at"), now)
.and_where(Expr::col(iden("code_hash")).eq(code_hash))
.and_where(Expr::col(iden("consumed_at")).is_null())
.and_where(Expr::col(iden("expires_at")).gt(now));
db.execute(&Statement::render(&update)).await
}
pub(crate) async fn unconsume_code(db: &dyn Database, code_hash: &str) -> Result<u64, DbError> {
let mut update = Query::update();
update
.table(iden(CODES))
.value(iden("consumed_at"), Option::<String>::None)
.and_where(Expr::col(iden("code_hash")).eq(code_hash))
.and_where(Expr::col(iden("consumed_at")).is_not_null());
db.execute(&Statement::render(&update)).await
}
pub(crate) async fn code_subject(
db: &dyn Database,
code_hash: &str,
) -> Result<Option<String>, DbError> {
let mut select = Query::select();
select
.column(iden("subject"))
.from(iden(CODES))
.and_where(Expr::col(iden("code_hash")).eq(code_hash));
Ok(db
.query(&Statement::render(&select))
.await?
.first()
.and_then(|row| row.get("subject")))
}
pub(crate) async fn subject_of_telegram_user(
db: &dyn Database,
telegram_user_id: i64,
) -> Result<Option<String>, DbError> {
let mut select = Query::select();
select
.column(iden("subject"))
.from(iden(LINKS))
.and_where(Expr::col(iden("telegram_user_id")).eq(telegram_user_id));
Ok(db
.query(&Statement::render(&select))
.await?
.first()
.and_then(|row| row.get("subject")))
}
pub(crate) async fn link_of_subject(
db: &dyn Database,
subject: &str,
) -> Result<Option<(i64, i64)>, DbError> {
let mut select = Query::select();
select
.columns(["telegram_user_id", "chat_id"])
.from(iden(LINKS))
.and_where(Expr::col(iden("subject")).eq(subject));
Ok(db
.query(&Statement::render(&select))
.await?
.first()
.map(|row: &Row| {
(
row.get::<i64>("telegram_user_id").unwrap_or_default(),
row.get::<i64>("chat_id").unwrap_or_default(),
)
}))
}
pub(crate) async fn upsert_link(
db: &dyn Database,
subject: &str,
telegram_user_id: i64,
chat_id: i64,
linked_at: &str,
) -> Result<u64, DbError> {
let mut insert = Query::insert();
insert
.into_table(iden(LINKS))
.columns(["subject", "telegram_user_id", "chat_id", "linked_at"])
.values_panic([
subject.into(),
telegram_user_id.into(),
chat_id.into(),
linked_at.into(),
]);
insert.on_conflict(
OnConflict::column(iden("subject"))
.update_columns(["telegram_user_id", "chat_id", "linked_at"])
.to_owned(),
);
db.execute(&Statement::render(&insert)).await
}
pub(crate) async fn delete_link(db: &dyn Database, subject: &str) -> Result<u64, DbError> {
let mut delete = Query::delete();
delete
.from_table(iden(LINKS))
.and_where(Expr::col(iden("subject")).eq(subject));
db.execute(&Statement::render(&delete)).await
}
pub(crate) struct ActionRow {
pub(crate) subject: String,
pub(crate) action: String,
pub(crate) value_moving: bool,
pub(crate) status: String,
pub(crate) expires_at: String,
}
fn row_from(row: &Row) -> ActionRow {
ActionRow {
subject: row.get("subject").unwrap_or_default(),
action: row.get("action").unwrap_or_default(),
value_moving: row.get::<i64>("value_moving").unwrap_or_default() != 0,
status: row.get("status").unwrap_or_default(),
expires_at: row.get("expires_at").unwrap_or_default(),
}
}
pub(crate) async fn insert_action(
db: &dyn Database,
action_id: &str,
subject: &str,
action: &str,
value_moving: bool,
expires_at: &str,
created_at: &str,
) -> Result<u64, DbError> {
let mut insert = Query::insert();
insert
.into_table(iden(ACTIONS))
.columns([
"action_id",
"subject",
"action",
"value_moving",
"status",
"expires_at",
"created_at",
])
.values_panic([
action_id.into(),
subject.into(),
action.into(),
i64::from(value_moving).into(),
"pending".into(),
expires_at.into(),
created_at.into(),
]);
db.execute(&Statement::render(&insert)).await
}
pub(crate) async fn action_by_id(
db: &dyn Database,
action_id: &str,
) -> Result<Option<ActionRow>, DbError> {
let mut select = Query::select();
select
.columns(["subject", "action", "value_moving", "status", "expires_at"])
.from(iden(ACTIONS))
.and_where(Expr::col(iden("action_id")).eq(action_id));
Ok(db
.query(&Statement::render(&select))
.await?
.first()
.map(row_from))
}
pub(crate) async fn decide_action(
db: &dyn Database,
action_id: &str,
to_status: &str,
now: &str,
) -> Result<u64, DbError> {
let mut update = Query::update();
update
.table(iden(ACTIONS))
.values([
(iden("status"), to_status.into()),
(iden("decided_at"), now.into()),
])
.and_where(Expr::col(iden("action_id")).eq(action_id))
.and_where(Expr::col(iden("status")).eq("pending"))
.and_where(Expr::col(iden("expires_at")).gt(now));
db.execute(&Statement::render(&update)).await
}
pub(crate) async fn confirm_action(
db: &dyn Database,
action_id: &str,
now: &str,
) -> Result<u64, DbError> {
let mut update = Query::update();
update
.table(iden(ACTIONS))
.values([
(iden("status"), "approved".into()),
(iden("decided_at"), now.into()),
])
.and_where(Expr::col(iden("action_id")).eq(action_id))
.and_where(Expr::col(iden("status")).eq("awaiting_passkey"))
.and_where(Expr::col(iden("expires_at")).gt(now));
db.execute(&Statement::render(&update)).await
}
pub(crate) async fn revert_confirm(db: &dyn Database, action_id: &str) -> Result<u64, DbError> {
revert_action_to(db, action_id, "approved", "awaiting_passkey").await
}
pub(crate) async fn revert_tap(
db: &dyn Database,
action_id: &str,
decided_status: &str,
) -> Result<u64, DbError> {
revert_action_to(db, action_id, decided_status, "pending").await
}
async fn revert_action_to(
db: &dyn Database,
action_id: &str,
from_status: &str,
to_status: &str,
) -> Result<u64, DbError> {
let mut update = Query::update();
update
.table(iden(ACTIONS))
.values([
(iden("status"), to_status.into()),
(iden("decided_at"), Option::<String>::None.into()),
])
.and_where(Expr::col(iden("action_id")).eq(action_id))
.and_where(Expr::col(iden("status")).eq(from_status));
db.execute(&Statement::render(&update)).await
}