taud/
task_info.rs

1/* This file is part of DarkFi (https://dark.fi)
2 *
3 * Copyright (C) 2020-2025 Dyne.org foundation
4 *
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU Affero General Public License as
7 * published by the Free Software Foundation, either version 3 of the
8 * License, or (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 * GNU Affero General Public License for more details.
14 *
15 * You should have received a copy of the GNU Affero General Public License
16 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
17 */
18
19use 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        // generate ref_id
323        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        // self.assign = assigns.to_owned();
424        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}