1use 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 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 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}