1use crate::texts::{self, Sealer};
16use serde::{Deserialize, Serialize};
17use std::path::{Path, PathBuf};
18
19pub const FILE: &str = "drafts.jsonl";
21
22#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
24pub struct TextDraft {
25 pub id: String,
27 pub to: String,
29 pub conversation: String,
31 pub body: String,
32 pub written: i64,
34 #[serde(default, skip_serializing_if = "String::is_empty")]
36 pub by: String,
37}
38
39pub fn path() -> PathBuf {
41 texts::private_folder().join(FILE)
42}
43
44pub fn all_in(file: &Path, sealer: &dyn Sealer) -> Vec<TextDraft> {
46 let mut drafts: Vec<TextDraft> = texts::read_file(file, sealer);
47 drafts.sort_by_key(|d| d.written);
48 drafts
49}
50
51pub fn all(sealer: &dyn Sealer) -> Vec<TextDraft> {
52 all_in(&path(), sealer)
53}
54
55pub fn add_in(file: &Path, draft: &TextDraft, sealer: &dyn Sealer) -> Result<(), String> {
57 texts::append(file, std::slice::from_ref(draft), sealer, Some(&|d: &TextDraft| d.id.clone())).map(|_| ())
58}
59
60pub fn add(draft: &TextDraft, sealer: &dyn Sealer) -> Result<(), String> {
61 add_in(&path(), draft, sealer)
62}
63
64pub fn remove_in(file: &Path, id: &str, sealer: &dyn Sealer) -> Result<bool, String> {
68 let gone = crate::calls::rewrite_lines(file, |line| sealer.open(line.trim()).and_then(|plain| serde_json::from_str::<TextDraft>(&plain).ok()).is_some_and(|d| d.id == id))?;
69 Ok(gone > 0)
70}
71
72pub fn remove(id: &str, sealer: &dyn Sealer) -> Result<bool, String> {
73 remove_in(&path(), id, sealer)
74}
75
76#[cfg(test)]
77mod tests {
78 use super::*;
79
80 struct Rot;
82 impl Sealer for Rot {
83 fn seal(&self, plain: &str) -> String {
84 format!("rot:{}", plain.chars().rev().collect::<String>())
85 }
86 fn open(&self, sealed: &str) -> Option<String> {
87 sealed.strip_prefix("rot:").map(|s| s.chars().rev().collect())
88 }
89 }
90
91 #[test]
92 fn drafts_wait_sealed_until_used_or_discarded() {
93 let dir = std::env::temp_dir().join(format!("sioul-textdraft-{}", std::process::id()));
94 let _ = std::fs::remove_dir_all(&dir);
95 let file = dir.join(FILE);
96 let one = TextDraft { id: "a".repeat(32), to: "01 99 00 12 34".into(), conversation: "+33199001234".into(), body: "See you at six.".into(), written: 2, by: "claude-code".into() };
97 let two = TextDraft { id: "b".repeat(32), written: 1, ..one.clone() };
98 add_in(&file, &one, &Rot).unwrap();
99 add_in(&file, &two, &Rot).unwrap();
100 add_in(&file, &one, &Rot).unwrap();
102 assert!(!std::fs::read_to_string(&file).unwrap().contains("See you at six"), "sealed at rest");
103 assert_eq!(all_in(&file, &Rot).iter().map(|d| d.written).collect::<Vec<_>>(), [1, 2]);
104 assert!(remove_in(&file, &one.id, &Rot).unwrap());
105 assert!(!remove_in(&file, &one.id, &Rot).unwrap());
106 assert_eq!(all_in(&file, &Rot), vec![two]);
107 std::fs::remove_dir_all(&dir).unwrap();
108 }
109}