1use std::{
20 collections::HashMap,
21 fmt,
22 path::{Path, PathBuf},
23 str::FromStr,
24};
25
26use darkfi_serial::{async_trait, SerialDecodable, SerialEncodable};
27use log::debug;
28use tinyjson::JsonValue;
29
30use darkfi::{
31 util::{
32 file::{load_json_file, save_json_file},
33 time::Timestamp,
34 },
35 Error,
36};
37
38use crate::{
39 error::{TaudError, TaudResult},
40 month_tasks::MonthTasks,
41 util::gen_id,
42};
43
44pub enum State {
45 Open,
46 Start,
47 Pause,
48 Stop,
49}
50
51impl State {
52 pub const fn is_start(&self) -> bool {
53 matches!(*self, Self::Start)
54 }
55 pub const fn is_pause(&self) -> bool {
56 matches!(*self, Self::Pause)
57 }
58 pub const fn is_stop(&self) -> bool {
59 matches!(*self, Self::Stop)
60 }
61}
62
63impl fmt::Display for State {
64 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
65 match self {
66 State::Open => write!(f, "open"),
67 State::Start => write!(f, "start"),
68 State::Stop => write!(f, "stop"),
69 State::Pause => write!(f, "pause"),
70 }
71 }
72}
73
74impl FromStr for State {
75 type Err = Error;
76
77 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
78 let result = match s.to_lowercase().as_str() {
79 "open" => State::Open,
80 "stop" => State::Stop,
81 "start" => State::Start,
82 "pause" => State::Pause,
83 _ => return Err(Error::ParseFailed("unable to parse state")),
84 };
85 Ok(result)
86 }
87}
88
89#[derive(Clone, Debug, SerialEncodable, SerialDecodable, PartialEq, Eq)]
90pub struct TaskEvent {
91 pub action: String,
92 pub author: String,
93 pub content: String,
94 pub timestamp: Timestamp,
95}
96
97impl std::fmt::Display for TaskEvent {
98 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
99 write!(f, "action: {}, timestamp: {}", self.action, self.timestamp)
100 }
101}
102
103impl Default for TaskEvent {
104 fn default() -> Self {
105 Self {
106 action: State::Open.to_string(),
107 author: "".to_string(),
108 content: "".to_string(),
109 timestamp: Timestamp::current_time(),
110 }
111 }
112}
113
114impl TaskEvent {
115 pub fn new(action: String, author: String, content: String) -> Self {
116 Self { action, author, content, timestamp: Timestamp::current_time() }
117 }
118}
119
120impl From<TaskEvent> for JsonValue {
121 fn from(task_event: TaskEvent) -> JsonValue {
122 JsonValue::Object(HashMap::from([
123 ("action".to_string(), JsonValue::String(task_event.action.clone())),
124 ("author".to_string(), JsonValue::String(task_event.author.clone())),
125 ("content".to_string(), JsonValue::String(task_event.content.clone())),
126 ("timestamp".to_string(), JsonValue::String(task_event.timestamp.inner().to_string())),
127 ]))
128 }
129}
130
131impl From<&JsonValue> for TaskEvent {
132 fn from(value: &JsonValue) -> TaskEvent {
133 let map = value.get::<HashMap<String, JsonValue>>().unwrap();
134 TaskEvent {
135 action: map["action"].get::<String>().unwrap().clone(),
136 author: map["author"].get::<String>().unwrap().clone(),
137 content: map["content"].get::<String>().unwrap().clone(),
138 timestamp: Timestamp::from_u64(
139 map["timestamp"].get::<String>().unwrap().parse::<u64>().unwrap(),
140 ),
141 }
142 }
143}
144
145#[derive(Clone, Debug, SerialDecodable, SerialEncodable, PartialEq, Eq)]
146pub struct Comment {
147 content: String,
148 author: String,
149 timestamp: Timestamp,
150}
151
152impl std::fmt::Display for Comment {
153 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
154 write!(f, "{} author: {}, content: {} ", self.timestamp, self.author, self.content)
155 }
156}
157
158impl From<Comment> for JsonValue {
159 fn from(comment: Comment) -> JsonValue {
160 JsonValue::Object(HashMap::from([
161 ("content".to_string(), JsonValue::String(comment.content.clone())),
162 ("author".to_string(), JsonValue::String(comment.author.clone())),
163 ("timestamp".to_string(), JsonValue::String(comment.timestamp.inner().to_string())),
164 ]))
165 }
166}
167
168impl From<JsonValue> for Comment {
169 fn from(value: JsonValue) -> Comment {
170 let map = value.get::<HashMap<String, JsonValue>>().unwrap();
171 Comment {
172 content: map["content"].get::<String>().unwrap().clone(),
173 author: map["author"].get::<String>().unwrap().clone(),
174 timestamp: Timestamp::from_u64(
175 map["timestamp"].get::<String>().unwrap().parse::<u64>().unwrap(),
176 ),
177 }
178 }
179}
180
181impl Comment {
182 pub fn new(content: &str, author: &str) -> Self {
183 Self {
184 content: content.into(),
185 author: author.into(),
186 timestamp: Timestamp::current_time(),
187 }
188 }
189}
190
191#[derive(Clone, Debug, SerialEncodable, SerialDecodable, PartialEq)]
192pub struct TaskInfo {
193 pub ref_id: String,
194 pub workspace: String,
195 pub title: String,
196 pub tags: Vec<String>,
197 pub desc: String,
198 pub owner: String,
199 pub assign: Vec<String>,
200 pub project: Vec<String>,
201 pub due: Option<Timestamp>,
202 pub rank: Option<f32>,
203 pub created_at: Timestamp,
204 pub state: String,
205 pub events: Vec<TaskEvent>,
206 pub comments: Vec<Comment>,
207}
208
209impl From<&TaskInfo> for JsonValue {
210 fn from(task: &TaskInfo) -> JsonValue {
211 let ref_id = JsonValue::String(task.ref_id.clone());
212 let workspace = JsonValue::String(task.workspace.clone());
213 let title = JsonValue::String(task.title.clone());
214 let tags: Vec<JsonValue> = task.tags.iter().map(|x| JsonValue::String(x.clone())).collect();
215 let desc = JsonValue::String(task.desc.clone());
216 let owner = JsonValue::String(task.owner.clone());
217
218 let assign: Vec<JsonValue> =
219 task.assign.iter().map(|x| JsonValue::String(x.clone())).collect();
220
221 let project: Vec<JsonValue> =
222 task.project.iter().map(|x| JsonValue::String(x.clone())).collect();
223
224 let due = if let Some(ts) = task.due {
225 JsonValue::String(ts.inner().to_string())
226 } else {
227 JsonValue::Null
228 };
229
230 let rank = if let Some(rank) = task.rank {
231 JsonValue::Number(rank.into())
232 } else {
233 JsonValue::Null
234 };
235
236 let created_at = JsonValue::String(task.created_at.inner().to_string());
237 let state = JsonValue::String(task.state.clone());
238 let events: Vec<JsonValue> = task.events.iter().map(|x| x.clone().into()).collect();
239 let comments: Vec<JsonValue> = task.comments.iter().map(|x| x.clone().into()).collect();
240
241 JsonValue::Object(HashMap::from([
242 ("ref_id".to_string(), ref_id),
243 ("workspace".to_string(), workspace),
244 ("title".to_string(), title),
245 ("tags".to_string(), JsonValue::Array(tags)),
246 ("desc".to_string(), desc),
247 ("owner".to_string(), owner),
248 ("assign".to_string(), JsonValue::Array(assign)),
249 ("project".to_string(), JsonValue::Array(project)),
250 ("due".to_string(), due),
251 ("rank".to_string(), rank),
252 ("created_at".to_string(), created_at),
253 ("state".to_string(), state),
254 ("events".to_string(), JsonValue::Array(events)),
255 ("comments".to_string(), JsonValue::Array(comments)),
256 ]))
257 }
258}
259
260impl From<JsonValue> for TaskInfo {
261 fn from(value: JsonValue) -> TaskInfo {
262 let tags = value["tags"].get::<Vec<JsonValue>>().unwrap();
263 let assign = value["assign"].get::<Vec<JsonValue>>().unwrap();
264 let project = value["project"].get::<Vec<JsonValue>>().unwrap();
265 let events = value["events"].get::<Vec<JsonValue>>().unwrap();
266 let comments = value["comments"].get::<Vec<JsonValue>>().unwrap();
267
268 let due = {
269 if value["due"].is_null() {
270 None
271 } else {
272 let u64_str = value["due"].get::<String>().unwrap();
273 Some(Timestamp::from_u64(u64_str.parse::<u64>().unwrap()))
274 }
275 };
276
277 let rank = {
278 if value["rank"].is_null() {
279 None
280 } else {
281 Some(*value["rank"].get::<f64>().unwrap() as f32)
282 }
283 };
284
285 let created_at = {
286 let u64_str = value["created_at"].get::<String>().unwrap();
287 Timestamp::from_u64(u64_str.parse::<u64>().unwrap())
288 };
289
290 let events: Vec<TaskEvent> = events.iter().map(|x| x.into()).collect();
291 let comments: Vec<Comment> = comments.iter().map(|x| (*x).clone().into()).collect();
292
293 TaskInfo {
294 ref_id: value["ref_id"].get::<String>().unwrap().clone(),
295 workspace: value["workspace"].get::<String>().unwrap().clone(),
296 title: value["title"].get::<String>().unwrap().clone(),
297 tags: tags.iter().map(|x| x.get::<String>().unwrap().clone()).collect(),
298 desc: value["desc"].get::<String>().unwrap().clone(),
299 owner: value["owner"].get::<String>().unwrap().clone(),
300 assign: assign.iter().map(|x| x.get::<String>().unwrap().clone()).collect(),
301 project: project.iter().map(|x| x.get::<String>().unwrap().clone()).collect(),
302 due,
303 rank,
304 created_at,
305 state: value["state"].get::<String>().unwrap().clone(),
306 events,
307 comments,
308 }
309 }
310}
311
312impl TaskInfo {
313 pub fn new(
314 workspace: String,
315 title: &str,
316 desc: &str,
317 owner: &str,
318 due: Option<Timestamp>,
319 rank: Option<f32>,
320 created_at: Timestamp,
321 ) -> TaudResult<Self> {
322 let ref_id = gen_id(30);
324
325 if let Some(d) = &due {
326 if *d < Timestamp::current_time() {
327 return Err(TaudError::InvalidDueTime)
328 }
329 }
330
331 Ok(Self {
332 ref_id,
333 workspace,
334 title: title.into(),
335 desc: desc.into(),
336 owner: owner.into(),
337 tags: vec![],
338 assign: vec![],
339 project: vec![],
340 due,
341 rank,
342 created_at,
343 state: "open".into(),
344 comments: vec![],
345 events: vec![],
346 })
347 }
348
349 pub fn load(ref_id: &str, dataset_path: &Path) -> TaudResult<Self> {
350 debug!(target: "tau", "TaskInfo::load()");
351 let task = load_json_file(&Self::get_path(ref_id, dataset_path))?;
352 Ok(task.into())
353 }
354
355 pub fn save(&self, dataset_path: &Path) -> TaudResult<()> {
356 debug!(target: "tau", "TaskInfo::save()");
357 save_json_file(&Self::get_path(&self.ref_id, dataset_path), &self.into(), true)
358 .map_err(TaudError::Darkfi)?;
359
360 if self.get_state() == "stop" {
361 self.deactivate(dataset_path)?;
362 } else {
363 self.activate(dataset_path)?;
364 }
365
366 Ok(())
367 }
368
369 pub fn activate(&self, path: &Path) -> TaudResult<()> {
370 debug!(target: "tau", "TaskInfo::activate()");
371 let mut mt = MonthTasks::load_or_create(Some(&self.created_at), path)?;
372 mt.add(&self.ref_id);
373 mt.save(path)
374 }
375
376 pub fn deactivate(&self, path: &Path) -> TaudResult<()> {
377 debug!(target: "tau", "TaskInfo::deactivate()");
378 let mut mt = MonthTasks::load_or_create(Some(&self.created_at), path)?;
379 mt.remove(&self.ref_id);
380 mt.save(path)
381 }
382
383 pub fn get_state(&self) -> String {
384 debug!(target: "tau", "TaskInfo::get_state()");
385 self.state.clone()
386 }
387
388 pub fn get_path(ref_id: &str, dataset_path: &Path) -> PathBuf {
389 debug!(target: "tau", "TaskInfo::get_path()");
390 dataset_path.join("task").join(ref_id)
391 }
392
393 pub fn get_ref_id(&self) -> String {
394 debug!(target: "tau", "TaskInfo::get_ref_id()");
395 self.ref_id.clone()
396 }
397
398 pub fn set_title(&mut self, title: &str) {
399 debug!(target: "tau", "TaskInfo::set_title()");
400 self.title = title.into();
401 }
402
403 pub fn set_desc(&mut self, desc: &str) {
404 debug!(target: "tau", "TaskInfo::set_desc()");
405 self.desc = desc.into();
406 }
407
408 pub fn set_tags(&mut self, tags: &[String]) {
409 debug!(target: "tau", "TaskInfo::set_tags()");
410 for tag in tags.iter() {
411 let stripped = &tag[1..];
412 if tag.starts_with('+') && !self.tags.contains(&stripped.to_string()) {
413 self.tags.push(stripped.to_string());
414 }
415 if tag.starts_with('-') {
416 self.tags.retain(|tag| tag != stripped);
417 }
418 }
419 }
420
421 pub fn set_assign(&mut self, assigns: &[String]) {
422 debug!(target: "tau", "TaskInfo::set_assign()");
423 for assign in assigns.iter() {
425 let stripped = assign.split('@').collect::<Vec<&str>>()[1];
426 if assign.starts_with('@') && !self.assign.contains(&stripped.to_string()) {
427 self.assign.push(stripped.to_string());
428 }
429 if assign.starts_with("-@") {
430 self.assign.retain(|assign| assign != stripped);
431 }
432 }
433 }
434
435 pub fn set_project(&mut self, projects: &[String]) {
436 debug!(target: "tau", "TaskInfo::set_project()");
437 projects.clone_into(&mut self.project);
438 }
439
440 pub fn set_comment(&mut self, c: Comment) {
441 debug!(target: "tau", "TaskInfo::set_comment()");
442 self.comments.push(c);
443 }
444
445 pub fn set_rank(&mut self, r: Option<f32>) {
446 debug!(target: "tau", "TaskInfo::set_rank()");
447 self.rank = r;
448 }
449
450 pub fn set_due(&mut self, d: Option<Timestamp>) {
451 debug!(target: "tau", "TaskInfo::set_due()");
452 self.due = d;
453 }
454
455 pub fn set_state(&mut self, state: &str) {
456 debug!(target: "tau", "TaskInfo::set_state()");
457 if self.get_state() == state {
458 return
459 }
460 self.state = state.to_string();
461 }
462}