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-07-10 11:29:48 +02:00
|
|
|
FIELD_DONE = "done"
|
2021-01-29 19:40:46 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
var (
|
2021-06-25 09:14:27 +02:00
|
|
|
KnownFolders = []string{
|
2021-01-30 11:20:12 +01:00
|
|
|
FOLDER_INBOX,
|
|
|
|
FOLDER_NEW,
|
|
|
|
FOLDER_RECURRING,
|
|
|
|
FOLDER_PLANNED,
|
|
|
|
FOLDER_UNPLANNED,
|
|
|
|
}
|
2021-06-04 10:45:56 +02:00
|
|
|
|
|
|
|
subjectFieldNames = []string{FIELD_ACTION}
|
|
|
|
bodyFieldNames = []string{
|
|
|
|
FIELD_ID,
|
|
|
|
FIELD_VERSION,
|
|
|
|
FIELD_ACTION,
|
|
|
|
FIELD_PROJECT,
|
|
|
|
FIELD_DUE,
|
|
|
|
FIELD_RECUR,
|
2021-07-10 11:29:48 +02:00
|
|
|
FIELD_DONE,
|
2021-06-04 10:45:56 +02:00
|
|
|
}
|
2021-01-29 17:22:07 +01:00
|
|
|
)
|
2021-01-29 12:29:23 +01:00
|
|
|
|
|
|
|
type Task struct {
|
2021-06-25 09:14:27 +02:00
|
|
|
// Message is the underlying message from which the task was created
|
|
|
|
// It only has meaning for remote repositories and will be nil in
|
|
|
|
// local situations. It will be filtered out in LocalRepository.SetTasks()
|
2021-06-04 10:45:56 +02:00
|
|
|
Message *mstore.Message
|
2021-01-29 17:22:07 +01:00
|
|
|
|
2021-06-04 10:45:56 +02:00
|
|
|
Id string
|
2021-01-30 11:20:12 +01:00
|
|
|
Version int
|
2021-06-04 10:45:56 +02:00
|
|
|
Folder string
|
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-07-10 11:29:48 +02:00
|
|
|
Done bool
|
2021-01-29 12:29:23 +01:00
|
|
|
}
|
|
|
|
|
2021-06-04 10:45:56 +02:00
|
|
|
func NewFromMessage(msg *mstore.Message) *Task {
|
|
|
|
t := &Task{
|
|
|
|
Folder: msg.Folder,
|
|
|
|
Message: msg,
|
2021-01-29 17:48:22 +01:00
|
|
|
}
|
2021-01-29 12:29:23 +01:00
|
|
|
|
2021-06-04 10:45:56 +02:00
|
|
|
// parse fields from message
|
|
|
|
subjectFields := map[string]string{}
|
|
|
|
for _, f := range subjectFieldNames {
|
|
|
|
subjectFields[f] = FieldFromSubject(f, msg.Subject)
|
2021-01-30 11:20:12 +01:00
|
|
|
}
|
|
|
|
|
2021-06-04 10:45:56 +02:00
|
|
|
bodyFields := map[string]string{}
|
|
|
|
for _, f := range bodyFieldNames {
|
|
|
|
value, _ := FieldFromBody(f, msg.Body)
|
|
|
|
bodyFields[f] = value
|
2021-01-29 17:48:22 +01:00
|
|
|
}
|
2021-01-29 12:29:23 +01:00
|
|
|
|
2021-06-04 10:45:56 +02:00
|
|
|
// apply precedence rules
|
|
|
|
version, _ := strconv.Atoi(bodyFields[FIELD_VERSION])
|
|
|
|
id := bodyFields[FIELD_ID]
|
|
|
|
if id == "" {
|
|
|
|
id = uuid.New().String()
|
|
|
|
version = 0
|
2021-01-30 15:25:25 +01:00
|
|
|
}
|
2021-06-04 10:45:56 +02:00
|
|
|
t.Id = id
|
|
|
|
t.Version = version
|
2021-01-30 15:25:25 +01:00
|
|
|
|
2021-06-04 10:45:56 +02:00
|
|
|
t.Action = bodyFields[FIELD_ACTION]
|
|
|
|
if t.Action == "" {
|
|
|
|
t.Action = subjectFields[FIELD_ACTION]
|
2021-01-31 10:01:03 +01:00
|
|
|
}
|
|
|
|
|
2021-06-04 10:45:56 +02:00
|
|
|
t.Project = bodyFields[FIELD_PROJECT]
|
|
|
|
t.Due = NewDateFromString(bodyFields[FIELD_DUE])
|
|
|
|
t.Recur = NewRecurrer(bodyFields[FIELD_RECUR])
|
2021-07-10 11:29:48 +02:00
|
|
|
t.Done = bodyFields[FIELD_DONE] == "true"
|
2021-01-29 12:29:23 +01:00
|
|
|
|
2021-06-04 10:45:56 +02:00
|
|
|
return t
|
|
|
|
}
|
2021-01-29 19:40:46 +01:00
|
|
|
|
2021-06-04 10:45:56 +02:00
|
|
|
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
|
2021-01-30 11:20:12 +01:00
|
|
|
}
|
2021-06-04 10:45:56 +02:00
|
|
|
}
|
2021-01-30 11:20:12 +01:00
|
|
|
|
2021-06-04 10:45:56 +02:00
|
|
|
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(),
|
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-07-11 12:02:16 +02:00
|
|
|
if t.Done {
|
|
|
|
fields[FIELD_DONE] = "true"
|
|
|
|
order = append(order, FIELD_DONE)
|
|
|
|
}
|
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-08-13 08:45:12 +02:00
|
|
|
body := fmt.Sprintf("\r\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]))
|
2021-08-13 08:45:12 +02:00
|
|
|
body += fmt.Sprintf("%s\r\n", line)
|
2021-01-29 12:29:23 +01:00
|
|
|
}
|
|
|
|
|
2021-01-29 18:10:06 +01:00
|
|
|
if t.Message != nil {
|
2021-08-13 08:45:12 +02:00
|
|
|
body += fmt.Sprintf("\r\nPrevious version:\r\n\r\n%s\r\n", t.Message.Body)
|
2021-01-29 18:10:06 +01:00
|
|
|
}
|
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
|
|
|
}
|