use crate::methods::submission::types::{EmailSubmission, UndoStatus};
use async_trait::async_trait;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::PathBuf;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(super) struct SubmissionAccountState {
pub submissions: HashMap<String, StoredSubmission>,
pub state_version: u64,
}
impl Default for SubmissionAccountState {
fn default() -> Self {
Self {
submissions: HashMap::new(),
state_version: 1,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct StoredSubmission {
#[serde(flatten)]
pub submission: EmailSubmission,
pub created_at: DateTime<Utc>,
}
#[async_trait]
pub trait SubmissionStore: Send + Sync {
async fn get_submission(
&self,
account_id: &str,
id: &str,
) -> anyhow::Result<Option<StoredSubmission>>;
async fn put_submission(&self, account_id: &str, entry: StoredSubmission)
-> anyhow::Result<()>;
async fn delete_submission(&self, account_id: &str, id: &str) -> anyhow::Result<()>;
async fn state_token(&self, account_id: &str) -> anyhow::Result<String>;
}
pub struct FileSubmissionStore {
base_dir: PathBuf,
}
impl FileSubmissionStore {
pub fn new(base_dir: impl Into<PathBuf>) -> Self {
Self {
base_dir: base_dir.into(),
}
}
fn account_path(&self, account_id: &str) -> PathBuf {
self.base_dir
.join("submissions")
.join(format!("{}.json", account_id))
}
async fn load(&self, account_id: &str) -> anyhow::Result<SubmissionAccountState> {
let path = self.account_path(account_id);
if !path.exists() {
return Ok(SubmissionAccountState::default());
}
let bytes = tokio::fs::read(&path).await?;
let state: SubmissionAccountState = serde_json::from_slice(&bytes)?;
Ok(state)
}
async fn save(&self, account_id: &str, state: &SubmissionAccountState) -> anyhow::Result<()> {
let path = self.account_path(account_id);
if let Some(parent) = path.parent() {
tokio::fs::create_dir_all(parent).await?;
}
let bytes = serde_json::to_vec_pretty(state)?;
tokio::fs::write(&path, bytes).await?;
Ok(())
}
}
#[async_trait]
impl SubmissionStore for FileSubmissionStore {
async fn get_submission(
&self,
account_id: &str,
id: &str,
) -> anyhow::Result<Option<StoredSubmission>> {
let state = self.load(account_id).await?;
Ok(state.submissions.get(id).cloned())
}
async fn put_submission(
&self,
account_id: &str,
entry: StoredSubmission,
) -> anyhow::Result<()> {
let mut state = self.load(account_id).await?;
state.submissions.insert(entry.submission.id.clone(), entry);
state.state_version += 1;
self.save(account_id, &state).await
}
async fn delete_submission(&self, account_id: &str, id: &str) -> anyhow::Result<()> {
let mut state = self.load(account_id).await?;
state.submissions.remove(id);
state.state_version += 1;
self.save(account_id, &state).await
}
async fn state_token(&self, account_id: &str) -> anyhow::Result<String> {
let state = self.load(account_id).await?;
Ok(state.state_version.to_string())
}
}
pub(super) fn within_undo_window(stored: &StoredSubmission, window_secs: i64) -> bool {
stored.submission.undo_status == UndoStatus::Pending
&& Utc::now() <= stored.created_at + chrono::Duration::seconds(window_secs)
}