package task import ( "errors" "fmt" "strconv" "strings" "git.ewintr.nl/gte/pkg/mstore" "github.com/google/uuid" ) var ( ErrOutdatedTask = errors.New("task is outdated") ErrTaskIsNotRecurring = errors.New("task is not recurring") ) const ( FOLDER_INBOX = "INBOX" FOLDER_NEW = "New" FOLDER_RECURRING = "Recurring" FOLDER_PLANNED = "Planned" FOLDER_UNPLANNED = "Unplanned" QUOTE_PREFIX = ">" PREVIOUS_SEPARATOR = "Previous version:" FIELD_SEPARATOR = ":" SUBJECT_SEPARATOR = " - " FIELD_ID = "id" FIELD_VERSION = "version" FIELD_ACTION = "action" FIELD_PROJECT = "project" FIELD_DUE = "due" FIELD_RECUR = "recur" ) var ( knownFolders = []string{ FOLDER_INBOX, FOLDER_NEW, FOLDER_RECURRING, FOLDER_PLANNED, FOLDER_UNPLANNED, } subjectFieldNames = []string{FIELD_ACTION} bodyFieldNames = []string{ FIELD_ID, FIELD_VERSION, FIELD_ACTION, FIELD_PROJECT, FIELD_DUE, FIELD_RECUR, } ) type Task struct { Message *mstore.Message Id string Version int Folder string Action string Project string Due Date Recur Recurrer } func NewFromMessage(msg *mstore.Message) *Task { t := &Task{ Folder: msg.Folder, Message: msg, } // parse fields from message subjectFields := map[string]string{} for _, f := range subjectFieldNames { subjectFields[f] = FieldFromSubject(f, msg.Subject) } bodyFields := map[string]string{} for _, f := range bodyFieldNames { value, _ := FieldFromBody(f, msg.Body) bodyFields[f] = value } // apply precedence rules version, _ := strconv.Atoi(bodyFields[FIELD_VERSION]) id := bodyFields[FIELD_ID] if id == "" { id = uuid.New().String() version = 0 } t.Id = id t.Version = version t.Action = bodyFields[FIELD_ACTION] if t.Action == "" { t.Action = subjectFields[FIELD_ACTION] } t.Project = bodyFields[FIELD_PROJECT] t.Due = NewDateFromString(bodyFields[FIELD_DUE]) t.Recur = NewRecurrer(bodyFields[FIELD_RECUR]) return t } func (t *Task) TargetFolder() string { switch { case t.Version == 0: return FOLDER_NEW case t.IsRecurrer(): return FOLDER_RECURRING case !t.Due.IsZero(): return FOLDER_PLANNED default: return FOLDER_UNPLANNED } } func (t *Task) NextMessage() *mstore.Message { tNew := t tNew.Folder = t.TargetFolder() tNew.Version++ return &mstore.Message{ Folder: tNew.Folder, Subject: tNew.FormatSubject(), Body: tNew.FormatBody(), } } func (t *Task) FormatSubject() string { var order []string if !t.Due.IsZero() { order = append(order, FIELD_DUE) } order = append(order, FIELD_PROJECT, FIELD_ACTION) fields := map[string]string{ FIELD_PROJECT: t.Project, FIELD_ACTION: t.Action, FIELD_DUE: t.Due.String(), } parts := []string{} for _, f := range order { if fields[f] != "" { parts = append(parts, fields[f]) } } return strings.Join(parts, SUBJECT_SEPARATOR) } func (t *Task) FormatBody() string { order := []string{FIELD_ACTION} fields := map[string]string{ FIELD_ID: t.Id, FIELD_VERSION: strconv.Itoa(t.Version), FIELD_PROJECT: t.Project, FIELD_ACTION: t.Action, } 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}...) keyLen := 0 for _, f := range order { if len(f) > keyLen { keyLen = len(f) } } body := fmt.Sprintf("\n") 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) } if t.Message != nil { body += fmt.Sprintf("\nPrevious version:\n\n%s\n", t.Message.Body) } return body } func (t *Task) IsRecurrer() bool { return t.Recur != nil } func (t *Task) RecursToday() bool { return t.RecursOn(Today) } func (t *Task) RecursOn(date Date) bool { if !t.IsRecurrer() { return false } return t.Recur.RecursOn(date) } func (t *Task) GenerateFromRecurrer(date Date) (*Task, error) { if !t.IsRecurrer() || !t.RecursOn(date) { return &Task{}, ErrTaskIsNotRecurring } return &Task{ Id: uuid.New().String(), Version: 1, Action: t.Action, Project: t.Project, Due: date, }, nil } func FieldFromBody(field, body string) (string, bool) { value := "" dirty := false lines := strings.Split(body, "\n") for _, line := range lines { line = strings.TrimSpace(strings.TrimPrefix(line, QUOTE_PREFIX)) if line == PREVIOUS_SEPARATOR { return value, dirty } parts := strings.SplitN(line, FIELD_SEPARATOR, 2) if len(parts) < 2 { continue } fieldName := strings.ToLower(strings.TrimSpace(parts[0])) if fieldName == field { if value == "" { value = lowerAndTrim(parts[1]) } else { dirty = true } } } return value, dirty } func FieldFromSubject(field, subject string) string { if field != FIELD_ACTION { return "" } terms := strings.Split(subject, SUBJECT_SEPARATOR) return lowerAndTrim(terms[len(terms)-1]) }