use sqlx::{Pool, Sqlite};
use crate::writer::epoch_ms_now;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Role {
Mod,
Admin,
}
impl Role {
pub(crate) fn as_str(self) -> &'static str {
match self {
Role::Mod => "mod",
Role::Admin => "admin",
}
}
pub(crate) fn from_db_str(s: &str) -> Option<Role> {
match s {
"mod" => Some(Role::Mod),
"admin" => Some(Role::Admin),
_ => None,
}
}
}
impl std::fmt::Display for Role {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone)]
pub struct Moderator {
pub did: String,
pub role: Role,
pub added_by: Option<String>,
pub added_at: i64,
}
#[derive(Debug, thiserror::Error)]
pub(crate) enum Error {
#[error("database error: {0}")]
Db(#[from] sqlx::Error),
#[error("corrupt moderator row: role value {0:?} violates CHECK constraint on moderators.role")]
CorruptRole(String),
}
pub(crate) type Result<T> = std::result::Result<T, Error>;
#[derive(Debug, PartialEq, Eq)]
pub(crate) enum AddOutcome {
Inserted,
RoleUpdated {
previous: Role,
},
Unchanged,
DuplicateBlocked {
current_role: Role,
},
}
pub(crate) async fn add(
pool: &Pool<Sqlite>,
did: &str,
role: Role,
added_by: Option<&str>,
allow_role_update: bool,
) -> Result<AddOutcome> {
let existing_role: Option<Role> =
sqlx::query_scalar!("SELECT role FROM moderators WHERE did = ?1", did)
.fetch_optional(pool)
.await?
.map(|s| Role::from_db_str(&s).ok_or(Error::CorruptRole(s)))
.transpose()?;
match existing_role {
Some(current) if current == role => Ok(AddOutcome::Unchanged),
Some(current) if !allow_role_update => Ok(AddOutcome::DuplicateBlocked {
current_role: current,
}),
Some(current) => {
let role_str = role.as_str();
sqlx::query!(
"UPDATE moderators SET role = ?1 WHERE did = ?2",
role_str,
did
)
.execute(pool)
.await?;
Ok(AddOutcome::RoleUpdated { previous: current })
}
None => {
let now = epoch_ms_now();
let role_str = role.as_str();
sqlx::query!(
"INSERT INTO moderators (did, role, added_by, added_at) VALUES (?1, ?2, ?3, ?4)",
did,
role_str,
added_by,
now,
)
.execute(pool)
.await?;
Ok(AddOutcome::Inserted)
}
}
}
#[derive(Debug, PartialEq, Eq)]
pub(crate) enum RemoveOutcome {
Removed,
NotFound,
}
pub(crate) async fn remove(pool: &Pool<Sqlite>, did: &str) -> Result<RemoveOutcome> {
let res = sqlx::query!("DELETE FROM moderators WHERE did = ?1", did)
.execute(pool)
.await?;
if res.rows_affected() == 0 {
Ok(RemoveOutcome::NotFound)
} else {
Ok(RemoveOutcome::Removed)
}
}
pub(crate) async fn list(pool: &Pool<Sqlite>, role_filter: Option<Role>) -> Result<Vec<Moderator>> {
let rows = match role_filter {
Some(r) => {
let role_str = r.as_str();
sqlx::query!(
"SELECT did, role, added_by, added_at FROM moderators
WHERE role = ?1 ORDER BY added_at ASC, did ASC",
role_str
)
.fetch_all(pool)
.await?
.into_iter()
.map(|r| (r.did, r.role, r.added_by, r.added_at))
.collect::<Vec<_>>()
}
None => sqlx::query!(
"SELECT did, role, added_by, added_at FROM moderators
ORDER BY added_at ASC, did ASC"
)
.fetch_all(pool)
.await?
.into_iter()
.map(|r| (r.did, r.role, r.added_by, r.added_at))
.collect::<Vec<_>>(),
};
rows.into_iter()
.map(|(did, role, added_by, added_at)| {
let role = Role::from_db_str(&role).ok_or(Error::CorruptRole(role))?;
Ok(Moderator {
did,
role,
added_by,
added_at,
})
})
.collect()
}
pub(crate) async fn count_admins(pool: &Pool<Sqlite>) -> Result<i64> {
let n = sqlx::query_scalar!("SELECT COUNT(*) FROM moderators WHERE role = 'admin'")
.fetch_one(pool)
.await?;
Ok(n)
}