taud/
task_info.rs

1/* This file is part of DarkFi (https://dark.fi)
2 *
3 * Copyright (C) 2020-2026 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 tinyjson::JsonValue;
28use tracing::debug;
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 bounty: Option<f32>,
206    pub events: Vec<TaskEvent>,
207    pub comments: Vec<Comment>,
208}
209
210impl From<&TaskInfo> for JsonValue {
211    fn from(task: &TaskInfo) -> JsonValue {
212        let ref_id = JsonValue::String(task.ref_id.clone());
213        let workspace = JsonValue::String(task.workspace.clone());
214        let title = JsonValue::String(task.title.clone());
215        let tags: Vec<JsonValue> = task.tags.iter().map(|x| JsonValue::String(x.clone())).collect();
216        let desc = JsonValue::String(task.desc.clone());
217        let owner = JsonValue::String(task.owner.clone());
218
219        let assign: Vec<JsonValue> =
220            task.assign.iter().map(|x| JsonValue::String(x.clone())).collect();
221
222        let project: Vec<JsonValue> =
223            task.project.iter().map(|x| JsonValue::String(x.clone())).collect();
224
225        let due = if let Some(ts) = task.due {
226            JsonValue::String(ts.inner().to_string())
227        } else {
228            JsonValue::Null
229        };
230
231        let rank = if let Some(rank) = task.rank {
232            JsonValue::Number(rank.into())
233        } else {
234            JsonValue::Null
235        };
236
237        let bounty = if let Some(bounty) = task.bounty {
238            JsonValue::Number(bounty.into())
239        } else {
240            JsonValue::Null
241        };
242
243        let created_at = JsonValue::String(task.created_at.inner().to_string());
244        let state = JsonValue::String(task.state.clone());
245        let events: Vec<JsonValue> = task.events.iter().map(|x| x.clone().into()).collect();
246        let comments: Vec<JsonValue> = task.comments.iter().map(|x| x.clone().into()).collect();
247
248        JsonValue::Object(HashMap::from([
249            ("ref_id".to_string(), ref_id),
250            ("workspace".to_string(), workspace),
251            ("title".to_string(), title),
252            ("tags".to_string(), JsonValue::Array(tags)),
253            ("desc".to_string(), desc),
254            ("owner".to_string(), owner),
255            ("assign".to_string(), JsonValue::Array(assign)),
256            ("project".to_string(), JsonValue::Array(project)),
257            ("due".to_string(), due),
258            ("rank".to_string(), rank),
259            ("created_at".to_string(), created_at),
260            ("state".to_string(), state),
261            ("bounty".to_string(), bounty),
262            ("events".to_string(), JsonValue::Array(events)),
263            ("comments".to_string(), JsonValue::Array(comments)),
264        ]))
265    }
266}
267
268impl From<JsonValue> for TaskInfo {
269    fn from(value: JsonValue) -> TaskInfo {
270        let tags = value["tags"].get::<Vec<JsonValue>>().unwrap();
271        let assign = value["assign"].get::<Vec<JsonValue>>().unwrap();
272        let project = value["project"].get::<Vec<JsonValue>>().unwrap();
273        let events = value["events"].get::<Vec<JsonValue>>().unwrap();
274        let comments = value["comments"].get::<Vec<JsonValue>>().unwrap();
275
276        let due = {
277            if value["due"].is_null() {
278                None
279            } else {
280                let u64_str = value["due"].get::<String>().unwrap();
281                Some(Timestamp::from_u64(u64_str.parse::<u64>().unwrap()))
282            }
283        };
284
285        let rank = {
286            if value["rank"].is_null() {
287                None
288            } else {
289                Some(*value["rank"].get::<f64>().unwrap() as f32)
290            }
291        };
292
293        let bounty = {
294            if value["bounty"].is_null() {
295                None
296            } else {
297                Some(*value["bounty"].get::<f64>().unwrap() as f32)
298            }
299        };
300
301        let created_at = {
302            let u64_str = value["created_at"].get::<String>().unwrap();
303            Timestamp::from_u64(u64_str.parse::<u64>().unwrap())
304        };
305
306        let events: Vec<TaskEvent> = events.iter().map(|x| x.into()).collect();
307        let comments: Vec<Comment> = comments.iter().map(|x| (*x).clone().into()).collect();
308
309        TaskInfo {
310            ref_id: value["ref_id"].get::<String>().unwrap().clone(),
311            workspace: value["workspace"].get::<String>().unwrap().clone(),
312            title: value["title"].get::<String>().unwrap().clone(),
313            tags: tags.iter().map(|x| x.get::<String>().unwrap().clone()).collect(),
314            desc: value["desc"].get::<String>().unwrap().clone(),
315            owner: value["owner"].get::<String>().unwrap().clone(),
316            assign: assign.iter().map(|x| x.get::<String>().unwrap().clone()).collect(),
317            project: project.iter().map(|x| x.get::<String>().unwrap().clone()).collect(),
318            due,
319            rank,
320            created_at,
321            state: value["state"].get::<String>().unwrap().clone(),
322            bounty,
323            events,
324            comments,
325        }
326    }
327}
328
329impl TaskInfo {
330    #[allow(clippy::too_many_arguments)]
331    pub fn new(
332        workspace: String,
333        title: &str,
334        desc: &str,
335        owner: &str,
336        due: Option<Timestamp>,
337        rank: Option<f32>,
338        created_at: Timestamp,
339        bounty: Option<f32>,
340    ) -> TaudResult<Self> {
341        // generate ref_id
342        let ref_id = gen_id(30);
343
344        if let Some(d) = &due {
345            if *d < Timestamp::current_time() {
346                return Err(TaudError::InvalidDueTime)
347            }
348        }
349
350        Ok(Self {
351            ref_id,
352            workspace,
353            title: title.into(),
354            desc: desc.into(),
355            owner: owner.into(),
356            tags: vec![],
357            assign: vec![],
358            project: vec![],
359            due,
360            rank,
361            created_at,
362            state: "open".into(),
363            bounty,
364            comments: vec![],
365            events: vec![],
366        })
367    }
368
369    pub fn load(ref_id: &str, dataset_path: &Path) -> TaudResult<Self> {
370        debug!(target: "tau", "TaskInfo::load()");
371        let task = load_json_file(&Self::get_path(ref_id, dataset_path))?;
372        Ok(task.into())
373    }
374
375    pub fn save(&self, dataset_path: &Path) -> TaudResult<()> {
376        debug!(target: "tau", "TaskInfo::save()");
377        save_json_file(&Self::get_path(&self.ref_id, dataset_path), &self.into(), true)
378            .map_err(TaudError::Darkfi)?;
379
380        if self.get_state() == "stop" {
381            self.deactivate(dataset_path)?;
382        } else {
383            self.activate(dataset_path)?;
384        }
385
386        Ok(())
387    }
388
389    pub fn activate(&self, path: &Path) -> TaudResult<()> {
390        debug!(target: "tau", "TaskInfo::activate()");
391        let mut mt = MonthTasks::load_or_create(Some(&self.created_at), path)?;
392        mt.add(&self.ref_id);
393        mt.save(path)
394    }
395
396    pub fn deactivate(&self, path: &Path) -> TaudResult<()> {
397        debug!(target: "tau", "TaskInfo::deactivate()");
398        let mut mt = MonthTasks::load_or_create(Some(&self.created_at), path)?;
399        mt.remove(&self.ref_id);
400        mt.save(path)
401    }
402
403    pub fn get_state(&self) -> String {
404        debug!(target: "tau", "TaskInfo::get_state()");
405        self.state.clone()
406    }
407
408    pub fn get_path(ref_id: &str, dataset_path: &Path) -> PathBuf {
409        debug!(target: "tau", "TaskInfo::get_path()");
410        dataset_path.join("task").join(ref_id)
411    }
412
413    pub fn get_ref_id(&self) -> String {
414        debug!(target: "tau", "TaskInfo::get_ref_id()");
415        self.ref_id.clone()
416    }
417
418    pub fn set_title(&mut self, title: &str) {
419        debug!(target: "tau", "TaskInfo::set_title()");
420        self.title = title.into();
421    }
422
423    pub fn set_desc(&mut self, desc: &str) {
424        debug!(target: "tau", "TaskInfo::set_desc()");
425        self.desc = desc.into();
426    }
427
428    pub fn set_tags(&mut self, tags: &[String]) {
429        debug!(target: "tau", "TaskInfo::set_tags()");
430        for tag in tags.iter() {
431            let stripped = &tag[1..];
432            if tag.starts_with('+') && !self.tags.contains(&stripped.to_string()) {
433                self.tags.push(stripped.to_string());
434            }
435            if tag.starts_with('-') {
436                self.tags.retain(|tag| tag != stripped);
437            }
438        }
439    }
440
441    pub fn set_assign(&mut self, assigns: &[String]) {
442        debug!(target: "tau", "TaskInfo::set_assign()");
443        // self.assign = assigns.to_owned();
444        for assign in assigns.iter() {
445            let stripped = assign.split('@').collect::<Vec<&str>>()[1];
446            if assign.starts_with('@') && !self.assign.contains(&stripped.to_string()) {
447                self.assign.push(stripped.to_string());
448            }
449            if assign.starts_with("-@") {
450                self.assign.retain(|assign| assign != stripped);
451            }
452        }
453    }
454
455    pub fn set_project(&mut self, projects: &[String]) {
456        debug!(target: "tau", "TaskInfo::set_project()");
457        projects.clone_into(&mut self.project);
458    }
459
460    pub fn set_comment(&mut self, c: Comment) {
461        debug!(target: "tau", "TaskInfo::set_comment()");
462        self.comments.push(c);
463    }
464
465    pub fn set_rank(&mut self, r: Option<f32>) {
466        debug!(target: "tau", "TaskInfo::set_rank()");
467        self.rank = r;
468    }
469
470    pub fn set_bounty(&mut self, b: Option<f32>) {
471        debug!(target: "tau", "TaskInfo::set_bounty()");
472        self.bounty = b;
473    }
474
475    pub fn set_due(&mut self, d: Option<Timestamp>) {
476        debug!(target: "tau", "TaskInfo::set_due()");
477        self.due = d;
478    }
479
480    pub fn set_state(&mut self, state: &str) {
481        debug!(target: "tau", "TaskInfo::set_state()");
482        if self.get_state() == state {
483            return
484        }
485        self.state = state.to_string();
486    }
487}