2021-01-29 12:29:23 +01:00
package task
import (
"errors"
"fmt"
2021-01-30 11:20:12 +01:00
"strconv"
2021-01-29 12:29:23 +01:00
"strings"
2021-05-13 09:47:06 +02:00
"git.ewintr.nl/gte/pkg/mstore"
2021-01-29 12:29:23 +01:00
"github.com/google/uuid"
)
var (
2021-01-31 10:01:03 +01:00
ErrOutdatedTask = errors . New ( "task is outdated" )
ErrTaskIsNotRecurring = errors . New ( "task is not recurring" )
2021-01-29 12:29:23 +01:00
)
2021-01-29 17:22:07 +01:00
const (
2021-01-30 11:20:12 +01:00
FOLDER_INBOX = "INBOX"
FOLDER_NEW = "New"
FOLDER_RECURRING = "Recurring"
FOLDER_PLANNED = "Planned"
FOLDER_UNPLANNED = "Unplanned"
2021-01-29 12:29:23 +01:00
2021-01-29 18:10:06 +01:00
QUOTE_PREFIX = ">"
PREVIOUS_SEPARATOR = "Previous version:"
2021-01-29 19:40:46 +01:00
FIELD_SEPARATOR = ":"
SUBJECT_SEPARATOR = " - "
FIELD_ID = "id"
2021-01-30 11:20:12 +01:00
FIELD_VERSION = "version"
2021-01-29 19:40:46 +01:00
FIELD_ACTION = "action"
FIELD_PROJECT = "project"
2021-01-30 11:20:12 +01:00
FIELD_DUE = "due"
2021-01-31 08:22:31 +01:00
FIELD_RECUR = "recur"
2021-01-29 19:40:46 +01:00
)
var (
2021-01-30 11:20:12 +01:00
knownFolders = [ ] string {
FOLDER_INBOX ,
FOLDER_NEW ,
FOLDER_RECURRING ,
FOLDER_PLANNED ,
FOLDER_UNPLANNED ,
}
2021-01-29 17:22:07 +01:00
)
2021-01-29 12:29:23 +01:00
2021-01-29 17:22:07 +01:00
// Task reperesents a task based on the data stored in a message
2021-01-29 12:29:23 +01:00
type Task struct {
2021-01-29 17:22:07 +01:00
// Id is a UUID that gets carried over when a new message is constructed
Id string
2021-01-30 11:20:12 +01:00
// Version is a method to determine the latest version for cleanup
Version int
2021-01-29 17:22:07 +01:00
// Folder is the same name as the mstore folder
Folder string
2021-01-29 19:40:46 +01:00
// Ordinary task attributes
2021-01-29 17:22:07 +01:00
Action string
2021-01-29 19:40:46 +01:00
Project string
2021-01-29 17:22:07 +01:00
Due Date
2021-01-31 08:22:31 +01:00
Recur Recurrer
2021-01-29 19:40:46 +01:00
2021-01-31 08:22:31 +01:00
//Message is the underlying message
2021-01-29 17:22:07 +01:00
Message * mstore . Message
// Current indicates whether the task represents an existing message in the mstore
Current bool
// Dirty indicates whether the task contains updates not present in the message
Dirty bool
2021-01-29 12:29:23 +01:00
}
2021-01-29 17:22:07 +01:00
// New constructs a Task based on an mstore.Message.
//
// The data in the message is stored as key: value pairs, one per line. The line can start with quoting marks.
// The subject line also contains values in the format "date - project - action".
// Keys that exist more than once are merged. The one that appears first in the body takes precedence. A value present in the Body takes precedence over one in the subject.
// This enables updating a task by forwarding a topposted message whith new values for fields that the user wants to update.
func New ( msg * mstore . Message ) * Task {
2021-01-29 19:40:46 +01:00
// Id
2021-01-29 17:22:07 +01:00
dirty := false
2021-01-30 15:25:25 +01:00
newId := false
2021-01-29 17:48:22 +01:00
id , d := FieldFromBody ( FIELD_ID , msg . Body )
2021-01-29 12:29:23 +01:00
if id == "" {
id = uuid . New ( ) . String ( )
2021-01-29 17:22:07 +01:00
dirty = true
2021-01-30 15:25:25 +01:00
newId = true
2021-01-29 12:29:23 +01:00
}
2021-01-29 17:48:22 +01:00
if d {
dirty = true
}
2021-01-29 12:29:23 +01:00
2021-01-30 11:20:12 +01:00
// Version, cannot manually be incremented from body
versionStr , _ := FieldFromBody ( FIELD_VERSION , msg . Body )
version , _ := strconv . Atoi ( versionStr )
if version == 0 {
dirty = true
}
2021-01-29 19:40:46 +01:00
// Action
2021-01-29 17:48:22 +01:00
action , d := FieldFromBody ( FIELD_ACTION , msg . Body )
2021-01-29 12:29:23 +01:00
if action == "" {
2021-01-29 17:22:07 +01:00
action = FieldFromSubject ( FIELD_ACTION , msg . Subject )
if action != "" {
dirty = true
}
2021-01-29 12:29:23 +01:00
}
2021-01-29 17:48:22 +01:00
if d {
dirty = true
}
2021-01-29 12:29:23 +01:00
2021-01-30 15:25:25 +01:00
// Due
dueStr , d := FieldFromBody ( FIELD_DUE , msg . Body )
2021-01-31 08:22:31 +01:00
if dueStr == "" {
dueStr = FieldFromSubject ( FIELD_DUE , msg . Subject )
if dueStr != "" {
dirty = true
}
}
if d {
2021-01-30 15:25:25 +01:00
dirty = true
}
due := NewDateFromString ( dueStr )
2021-01-31 10:01:03 +01:00
// Recurrer
recurStr , d := FieldFromBody ( FIELD_RECUR , msg . Body )
if d {
dirty = true
}
recur := NewRecurrer ( recurStr )
2021-01-29 19:40:46 +01:00
// Folder
2021-01-30 15:25:25 +01:00
folderOld := msg . Folder
folderNew := folderOld
if folderOld == FOLDER_INBOX {
switch {
case newId :
folderNew = FOLDER_NEW
2021-01-31 10:01:03 +01:00
case ! newId && recur != nil :
folderNew = FOLDER_RECURRING
case ! newId && recur == nil && due . IsZero ( ) :
2021-01-30 15:25:25 +01:00
folderNew = FOLDER_UNPLANNED
2021-01-31 10:01:03 +01:00
case ! newId && recur == nil && ! due . IsZero ( ) :
2021-01-30 15:25:25 +01:00
folderNew = FOLDER_PLANNED
}
2021-01-31 10:01:03 +01:00
2021-01-30 15:25:25 +01:00
}
if folderOld != folderNew {
2021-01-29 17:22:07 +01:00
dirty = true
2021-01-29 12:29:23 +01:00
}
2021-01-29 19:40:46 +01:00
// Project
project , d := FieldFromBody ( FIELD_PROJECT , msg . Body )
2021-01-31 08:22:31 +01:00
if project == "" {
project = FieldFromSubject ( FIELD_PROJECT , msg . Subject )
if project != "" {
dirty = true
}
}
2021-01-29 19:40:46 +01:00
if d {
dirty = true
}
2021-01-30 11:20:12 +01:00
if dirty {
version ++
}
2021-01-29 12:29:23 +01:00
return & Task {
2021-01-29 17:22:07 +01:00
Id : id ,
2021-01-30 11:20:12 +01:00
Version : version ,
2021-01-30 15:25:25 +01:00
Folder : folderNew ,
2021-01-29 19:40:46 +01:00
Action : action ,
2021-01-30 15:25:25 +01:00
Due : due ,
2021-01-31 10:01:03 +01:00
Recur : recur ,
2021-01-29 19:40:46 +01:00
Project : project ,
2021-01-29 17:22:07 +01:00
Message : msg ,
Current : true ,
Dirty : dirty ,
2021-01-29 12:29:23 +01:00
}
}
2021-01-29 17:22:07 +01:00
func ( t * Task ) FormatSubject ( ) string {
2021-01-30 15:25:25 +01:00
var order [ ] string
if ! t . Due . IsZero ( ) {
order = append ( order , FIELD_DUE )
}
order = append ( order , FIELD_PROJECT , FIELD_ACTION )
2021-01-29 19:40:46 +01:00
fields := map [ string ] string {
FIELD_PROJECT : t . Project ,
FIELD_ACTION : t . Action ,
2021-01-30 15:25:25 +01:00
FIELD_DUE : t . Due . String ( ) ,
2021-01-29 19:40:46 +01:00
}
parts := [ ] string { }
for _ , f := range order {
if fields [ f ] != "" {
parts = append ( parts , fields [ f ] )
}
}
return strings . Join ( parts , SUBJECT_SEPARATOR )
2021-01-29 17:22:07 +01:00
}
2021-01-29 12:29:23 +01:00
2021-01-29 17:22:07 +01:00
func ( t * Task ) FormatBody ( ) string {
2021-01-31 10:01:03 +01:00
order := [ ] string { FIELD_ACTION }
2021-01-29 17:22:07 +01:00
fields := map [ string ] string {
2021-01-29 19:40:46 +01:00
FIELD_ID : t . Id ,
2021-01-30 11:20:12 +01:00
FIELD_VERSION : strconv . Itoa ( t . Version ) ,
2021-01-29 19:40:46 +01:00
FIELD_PROJECT : t . Project ,
FIELD_ACTION : t . Action ,
2021-01-29 12:29:23 +01:00
}
2021-01-31 10:01:03 +01:00
if t . IsRecurrer ( ) {
order = append ( order , FIELD_RECUR )
fields [ FIELD_RECUR ] = t . Recur . String ( )
} else {
order = append ( order , FIELD_DUE )
fields [ FIELD_DUE ] = t . Due . String ( )
}
order = append ( order , [ ] string { FIELD_PROJECT , FIELD_VERSION , FIELD_ID } ... )
2021-01-29 12:29:23 +01:00
2021-01-29 17:22:07 +01:00
keyLen := 0
for _ , f := range order {
if len ( f ) > keyLen {
keyLen = len ( f )
}
2021-01-29 12:29:23 +01:00
}
2021-01-31 10:01:03 +01:00
body := fmt . Sprintf ( "\n" )
2021-01-29 17:22:07 +01:00
for _ , f := range order {
key := f + FIELD_SEPARATOR
for i := len ( key ) ; i <= keyLen ; i ++ {
key += " "
}
line := strings . TrimSpace ( fmt . Sprintf ( "%s %s" , key , fields [ f ] ) )
body += fmt . Sprintf ( "%s\n" , line )
2021-01-29 12:29:23 +01:00
}
2021-01-29 18:10:06 +01:00
if t . Message != nil {
body += fmt . Sprintf ( "\nPrevious version:\n\n%s\n" , t . Message . Body )
}
2021-01-29 12:29:23 +01:00
return body
}
2021-01-31 10:01:03 +01:00
func ( t * Task ) IsRecurrer ( ) bool {
return t . Recur != nil
}
func ( t * Task ) RecursToday ( ) bool {
2021-02-05 10:04:19 +01:00
return t . RecursOn ( Today )
}
func ( t * Task ) RecursOn ( date Date ) bool {
2021-01-31 10:01:03 +01:00
if ! t . IsRecurrer ( ) {
return false
}
2021-02-05 10:04:19 +01:00
return t . Recur . RecursOn ( date )
2021-01-31 10:01:03 +01:00
}
2021-05-13 08:15:14 +02:00
func ( t * Task ) GenerateFromRecurrer ( date Date ) ( * Task , error ) {
if ! t . IsRecurrer ( ) || ! t . RecursOn ( date ) {
return & Task { } , ErrTaskIsNotRecurring
2021-01-31 10:01:03 +01:00
}
2021-05-13 08:15:14 +02:00
return & Task {
2021-01-31 10:01:03 +01:00
Id : uuid . New ( ) . String ( ) ,
Version : 1 ,
Action : t . Action ,
Project : t . Project ,
2021-01-31 12:11:02 +01:00
Due : date ,
2021-05-13 08:15:14 +02:00
} , nil
2021-01-31 10:01:03 +01:00
}
2021-01-29 17:48:22 +01:00
func FieldFromBody ( field , body string ) ( string , bool ) {
value := ""
dirty := false
2021-01-29 12:29:23 +01:00
lines := strings . Split ( body , "\n" )
for _ , line := range lines {
2021-01-29 18:10:06 +01:00
line = strings . TrimSpace ( strings . TrimPrefix ( line , QUOTE_PREFIX ) )
if line == PREVIOUS_SEPARATOR {
return value , dirty
}
2021-01-29 17:22:07 +01:00
parts := strings . SplitN ( line , FIELD_SEPARATOR , 2 )
2021-01-29 12:29:23 +01:00
if len ( parts ) < 2 {
continue
}
2021-01-29 17:48:22 +01:00
2021-01-29 18:10:06 +01:00
fieldName := strings . ToLower ( strings . TrimSpace ( parts [ 0 ] ) )
2021-01-29 17:48:22 +01:00
if fieldName == field {
if value == "" {
2021-01-31 13:31:35 +01:00
value = lowerAndTrim ( parts [ 1 ] )
2021-01-29 17:48:22 +01:00
} else {
dirty = true
}
2021-01-29 12:29:23 +01:00
}
}
2021-01-29 17:48:22 +01:00
return value , dirty
2021-01-29 12:29:23 +01:00
}
func FieldFromSubject ( field , subject string ) string {
2021-02-01 14:20:41 +01:00
if field != FIELD_ACTION {
return ""
2021-01-31 08:22:31 +01:00
}
2021-01-30 11:20:12 +01:00
2021-02-01 14:20:41 +01:00
terms := strings . Split ( subject , SUBJECT_SEPARATOR )
return lowerAndTrim ( terms [ len ( terms ) - 1 ] )
2021-01-30 11:20:12 +01:00
}