diff --git a/pkg/caldav/parsing.go b/pkg/caldav/parsing.go index 56838e455f..40f49fd452 100644 --- a/pkg/caldav/parsing.go +++ b/pkg/caldav/parsing.go @@ -22,6 +22,7 @@ import ( "strings" "time" + "code.vikunja.io/api/pkg/config" "code.vikunja.io/api/pkg/log" "code.vikunja.io/api/pkg/models" "code.vikunja.io/api/pkg/utils" @@ -88,15 +89,15 @@ func ParseTaskFromVTODO(content string) (vTask *models.Task, err error) { return nil, errors.New("VTODO element not found") } // We put the vTodo details in a map to be able to handle them more easily - task := make(map[string]string) + task := make(map[string]ics.IANAProperty) for _, c := range vTodo.UnknownPropertiesIANAProperties() { - task[c.IANAToken] = c.Value + task[c.IANAToken] = c } // Parse the priority var priority int64 if _, ok := task["PRIORITY"]; ok { - priorityParsed, err := strconv.ParseInt(task["PRIORITY"], 10, 64) + priorityParsed, err := strconv.ParseInt(task["PRIORITY"].Value, 10, 64) if err != nil { return nil, err } @@ -105,14 +106,14 @@ func ParseTaskFromVTODO(content string) (vTask *models.Task, err error) { } // Parse the enddate - duration, _ := time.ParseDuration(task["DURATION"]) + duration, _ := time.ParseDuration(task["DURATION"].Value) - description := strings.ReplaceAll(task["DESCRIPTION"], "\\,", ",") + description := strings.ReplaceAll(task["DESCRIPTION"].Value, "\\,", ",") description = strings.ReplaceAll(description, "\\n", "\n") var labels []*models.Label if val, ok := task["CATEGORIES"]; ok { - categories := strings.Split(val, ",") + categories := strings.Split(val.Value, ",") labels = make([]*models.Label, 0, len(categories)) for _, category := range categories { labels = append(labels, &models.Label{ @@ -122,8 +123,8 @@ func ParseTaskFromVTODO(content string) (vTask *models.Task, err error) { } vTask = &models.Task{ - UID: task["UID"], - Title: task["SUMMARY"], + UID: task["UID"].Value, + Title: task["SUMMARY"].Value, Description: description, Priority: priority, Labels: labels, @@ -133,7 +134,7 @@ func ParseTaskFromVTODO(content string) (vTask *models.Task, err error) { DoneAt: caldavTimeToTimestamp(task["COMPLETED"]), } - if task["STATUS"] == "COMPLETED" { + if task["STATUS"].Value == "COMPLETED" { vTask.Done = true } @@ -159,7 +160,7 @@ func parseVAlarm(vAlarm *ics.VAlarm, vTask *models.Task) *models.Task { if contains(property.ICalParameters["VALUE"], "DATE-TIME") { // Example: TRIGGER;VALUE=DATE-TIME:20181201T011210Z vTask.Reminders = append(vTask.Reminders, &models.TaskReminder{ - Reminder: caldavTimeToTimestamp(property.Value), + Reminder: caldavTimeToTimestamp(property), }) continue } @@ -199,7 +200,8 @@ func contains(array []string, str string) bool { } // https://tools.ietf.org/html/rfc5545#section-3.3.5 -func caldavTimeToTimestamp(tstring string) time.Time { +func caldavTimeToTimestamp(ianaProperty ics.IANAProperty) time.Time { + tstring := ianaProperty.Value if tstring == "" { return time.Time{} } @@ -214,7 +216,24 @@ func caldavTimeToTimestamp(tstring string) time.Time { format = `20060102` } - t, err := time.Parse(format, tstring) + var t time.Time + var err error + tzParameter := ianaProperty.ICalParameters["TZID"] + if len(tzParameter) > 0 { + loc, err := time.LoadLocation(tzParameter[0]) + if err != nil { + log.Warningf("Error while parsing caldav timezone %s: %s", tzParameter[0], err) + } else { + t, err = time.ParseInLocation(format, tstring, loc) + if err != nil { + log.Warningf("Error while parsing caldav time %s to TimeStamp: %s at location %s", tstring, loc, err) + } else { + t = t.In(config.GetTimeZone()) + return t + } + } + } + t, err = time.Parse(format, tstring) if err != nil { log.Warningf("Error while parsing caldav time %s to TimeStamp: %s", tstring, err) return time.Time{} diff --git a/pkg/caldav/parsing_test.go b/pkg/caldav/parsing_test.go index b513fd0b31..c2b7b274a5 100644 --- a/pkg/caldav/parsing_test.go +++ b/pkg/caldav/parsing_test.go @@ -219,6 +219,76 @@ END:VCALENDAR`, Updated: time.Unix(1543626724, 0).In(config.GetTimeZone()), }, }, + { + name: "example task from tasks.org app", + args: args{content: `BEGIN:VCALENDAR +VERSION:2.0 +PRODID:+//IDN tasks.org//android-130102//EN +BEGIN:VTODO +DTSTAMP:20230402T074158Z +UID:4290517349243274514 +CREATED:20230402T060451Z +LAST-MODIFIED:20230402T074154Z +SUMMARY:Test with tasks.org +PRIORITY:9 +CATEGORIES:Vikunja +X-APPLE-SORT-ORDER:697384109 +DUE;TZID=Europe/Berlin:20230402T170001 +DTSTART;TZID=Europe/Berlin:20230401T090000 +BEGIN:VALARM +TRIGGER;RELATED=END:PT0S +ACTION:DISPLAY +DESCRIPTION:Default Tasks.org description +END:VALARM +BEGIN:VALARM +TRIGGER;VALUE=DATE-TIME:20230402T100000Z +ACTION:DISPLAY +DESCRIPTION:Default Tasks.org description +END:VALARM +END:VTODO +BEGIN:VTIMEZONE +TZID:Europe/Berlin +LAST-MODIFIED:20220816T024022Z +BEGIN:DAYLIGHT +TZNAME:CEST +TZOFFSETFROM:+0100 +TZOFFSETTO:+0200 +DTSTART:19810329T020000 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:CET +TZOFFSETFROM:+0200 +TZOFFSETTO:+0100 +DTSTART:19961027T030000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU +END:STANDARD +END:VTIMEZONE +END:VCALENDAR`, + }, + wantVTask: &models.Task{ + Updated: time.Date(2023, 4, 2, 7, 41, 58, 0, config.GetTimeZone()), + UID: "4290517349243274514", + Title: "Test with tasks.org", + Priority: 1, + Labels: []*models.Label{ + { + Title: "Vikunja", + }, + }, + DueDate: time.Date(2023, 4, 2, 15, 0, 1, 0, config.GetTimeZone()), + StartDate: time.Date(2023, 4, 1, 7, 0, 0, 0, config.GetTimeZone()), + Reminders: []*models.TaskReminder{ + { + RelativeTo: models.ReminderRelationDueDate, + RelativePeriod: 0, + }, + { + Reminder: time.Date(2023, 4, 2, 10, 0, 0, 0, config.GetTimeZone()), + }, + }, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) {