tempus/caldav.go

498 lines
12 KiB
Go
Raw Normal View History

2024-06-19 19:27:13 +00:00
package main
import (
"context"
// "fmt"
webdav "github.com/emersion/go-webdav"
"github.com/emersion/go-webdav/caldav"
"github.com/emersion/go-ical"
2024-06-19 19:27:13 +00:00
"strings"
"github.com/google/uuid"
2024-06-21 18:38:41 +00:00
"time"
"errors"
// "fmt"
2024-06-19 19:27:13 +00:00
)
// type TODO struct {
// Name string
// Desc string
// Time string //only parsed _time_
// // Priority int //TODO
// // Subtasks []TODO //TODO
// // Repeat //TODO
// // Alarm //TODO
// UID string //for editing events
// DueDateTime time.Time //for adding new events
// }
2024-06-19 19:27:13 +00:00
//TODO Is it safe to create global variables?
2024-06-19 19:27:13 +00:00
var clientWebDAV *webdav.Client
var client *caldav.Client // clientCalDAV
// var calendarObjects []caldav.CalendarObject
var ctx = context.Background()
2024-06-21 18:38:41 +00:00
2024-06-19 19:27:13 +00:00
// var authSession caldav.Client // clientCalDAV
func InitDAVclients(url,user,pass string) error {
2024-06-21 18:38:41 +00:00
2024-06-19 19:27:13 +00:00
var err error
authSession := webdav.HTTPClientWithBasicAuth(nil, user,pass)
2024-06-19 19:27:13 +00:00
clientWebDAV, err = webdav.NewClient(authSession, url)
if err != nil {
// Handle error
return err
}
client, err = caldav.NewClient(authSession, url)
2024-06-19 19:27:13 +00:00
if err != nil {
// Handle error
return err
}
return nil
}
func (options *Options) InitDAVclients() error {
err := InitDAVclients(options.URL,options.User, options.Password)
if err != nil {
// Handle error
return err
}
return nil
}
func (m model) InitDAVclients() error {
err := InitDAVclients(m.Creds.URL, m.Creds.Username, m.Creds.Password)
2024-06-19 19:27:13 +00:00
if err != nil {
// Handle error
return err
}
return nil
}
2024-06-21 18:38:41 +00:00
func GetCalendars() ([]caldav.Calendar, error) {
2024-06-19 19:27:13 +00:00
principal, err := clientWebDAV.FindCurrentUserPrincipal(ctx)
if err != nil {
// Handle error
2024-06-21 18:38:41 +00:00
return nil, err
2024-06-19 19:27:13 +00:00
}
// fmt.Println("principal: ",principal) TODO log
// Find the calendar home set
calendarHomeSet, err := client.FindCalendarHomeSet(ctx, principal)
if err != nil {
2024-06-21 18:38:41 +00:00
return nil, err
2024-06-19 19:27:13 +00:00
}
// Find calendars in the calendar home set
calendars, err := client.FindCalendars(ctx, calendarHomeSet)
if err != nil {
2024-06-21 18:38:41 +00:00
return nil, err
2024-06-19 19:27:13 +00:00
}
return calendars, nil
}
2024-06-21 18:38:41 +00:00
func GetTODOs(calendarPath string) (calendarObjects []caldav.CalendarObject, err error) {
//TODO can we use calendar.Data.Component.Children to dont make this close to pointless request?
2024-06-19 19:27:13 +00:00
date := time.Now()
2024-06-21 18:38:41 +00:00
dateStart := time.Date(date.Year(), date.Month(), date.Day(), 23, 59, 59, 0, date.Location()).AddDate(0, 0, -1) //TODO too complex - time.Now().Add(-92 * time.Hour),
2024-06-19 19:27:13 +00:00
// dateEnd:= time.Date(date.Year(), date.Month(), date.Day(), 23, 59, 59, 0, date.Location()) //date +1 day
2024-06-21 18:38:41 +00:00
dateEnd := date.AddDate(0, 1, 0) //TODO we hard limit pulling events only for this month, to prevent getting tooo much events
2024-06-19 19:27:13 +00:00
calQuery := caldav.CalendarQuery{
CompRequest: caldav.CalendarCompRequest{
2024-06-21 18:38:41 +00:00
Name: "VCALENDAR",
Comps: []caldav.CalendarCompRequest{{
Name: "VTODO",
Props: []string{
"UID",
"SUMMARY",
"COMPLETED",
"DESCRIPTION",
"DUE",
"PRIORITY",
"RELATED-TO",
},
},
},
},
2024-06-19 19:27:13 +00:00
CompFilter: caldav.CompFilter{
2024-06-21 18:38:41 +00:00
Name: "VCALENDAR",
Comps: []caldav.CompFilter{
{
Name: "VTODO",
Start: dateStart,
End: dateEnd,
// FYI: server-side filtering is not supported for Nextcloud
2024-06-19 19:27:13 +00:00
},
2024-06-21 18:38:41 +00:00
},
},
2024-06-19 19:27:13 +00:00
}
2024-06-21 18:38:41 +00:00
calendarObjects, err = client.QueryCalendar(ctx, calendarPath, &calQuery)
2024-06-19 19:27:13 +00:00
if err != nil {
2024-06-21 18:38:41 +00:00
return nil, err
2024-06-19 19:27:13 +00:00
}
2024-06-21 18:38:41 +00:00
return calendarObjects, nil
2024-06-19 19:27:13 +00:00
}
// func ParseDueDateTODOs_depricated(calObjs []caldav.CalendarObject, date time.Time) ([]TODO, error) {
// var output []TODO
//
// for _, calObj := range calObjs {
// // fmt.Println((*(*calObj.Data).Children[0]).Name)
// // TODO STATUS map[] COMPLETED
// for _, event := range (*calObj.Data).Children {
// // if (*event).Name == "VTODO" {
// // var notCompletedTODO, withDate, fromToday bool
// var notCompletedTODO, fromToday bool
// //TODO we can optimize there if we encounter wrong state to forcefully stop next analysis
// //TODO we can use event.Props.Get
// // notCompletedTODO
// if (*event).Props["COMPLETED"] == nil {
// if (*event).Props["STATUS"] == nil {
// notCompletedTODO = true
// } else {
// if (*event).Props["STATUS"][0].Value != "COMPLETED" {
// notCompletedTODO = true
// }
// }
// }
//
// // withTodayDate
// if (*event).Props["DUE"] != nil {
// // withDate = true
// // fromToday
// if strings.HasPrefix((*event).Props["DUE"][0].Value, date.Format("20060102")) {
// fromToday = true
// }
// }
//
// // if notCompletedTODO && withDate && fromToday {
// if notCompletedTODO && fromToday {
//
// var tmpTODO TODO
//
// if (*event).Props["SUMMARY"] != nil {
// name := (*event).Props["SUMMARY"][0].Value
// tmpTODO.Name = name
// } else {
// tmpTODO.Name = "<EMPTY>"
// }
// if (*event).Props["DESCRIPTION"] != nil {
// description := (*event).Props["DESCRIPTION"][0].Value
// tmpTODO.Desc = description
// }
//
// var todoTime string
// due := (*event).Props["DUE"][0].Value
// index := strings.Index(due, "T")
// if index != -1 {
// str := due[index+1:]
// todoTime = str[:2] + ":" + str[2:4]
//
// tmpTODO.Time = todoTime
// }
// output = append(output, tmpTODO)
// }
// // }
// }
// }
//
// // //TODO sort: time, priority (if no time)
// // // it means, put DUE at first, other DUE;VALUE=DATE in priority order
// // //TODO color: priority, add time icon if have time
// // //TODO UID:7ed30f40-fce1-422c-be3b-0486dcfe8943
// // //TODO RELATED-TO:7ed30f40-fce1-422c-be3b-0486dcfe8943 # subtask
// // //TODO PRIORITY:1 #1-high, 5-mid, 9-low
// //TODO repeat function???
//
// //TODO on complete -repeat function
// // RRULE:FREQ=WEEKLY;INTERVAL=1
//
// //TODO if no repeat - mark as complted
// // STATUS:COMPLETED
// // COMPLETED:20240421T065323Z
// // PERCENT-COMPLETE:100
//
// //TODO support notifcations/alarms???
// // BEGIN:VTODO
// // ...
// // BEGIN:VALARM
// // TRIGGER;RELATED=END:-PT15M
// // ACTION:DISPLAY
// // DESCRIPTION:Default Tasks.org description
// // END:VALARM
// // END:VTODO
//
// return output, nil
// }
func ParseDueDateTODOs(calObjs []caldav.CalendarObject, date time.Time) ([]ical.Event, error) {
var output []ical.Event
2024-06-19 19:27:13 +00:00
2024-06-21 18:38:41 +00:00
for _, calObj := range calObjs {
2024-06-19 19:27:13 +00:00
// fmt.Println((*(*calObj.Data).Children[0]).Name)
// TODO STATUS map[] COMPLETED
// for _, event := range (*calObj.Data).Children {
// event
// for _, event1 := range (*calObj.Data).Children {
// // fmt.Println("1:", event1)
// tmp := ical.Event{Component: event1}
// fmt.Println(tmp.Props.Get(ical.PropUID).Text())
// // for _, event2 := range tmp.Events() {
// // // for _, event2 := range event1.Events() {
// // fmt.Println("1:", event2)
// // }
// }
for _, eventComponent:= range (*calObj.Data).Children {
event := ical.Event{Component: eventComponent}
// for _, event := range (*calObj.Data).Children {
// fmt.Println("1:", event) //TODO rm me
2024-06-19 19:27:13 +00:00
// if (*event).Name == "VTODO" {
// var notCompletedTODO, withDate, fromToday bool
var notCompletedTODO, fromToday bool
2024-06-21 18:38:41 +00:00
//TODO we can optimize there if we encounter wrong state to forcefully stop next analysis
//TODO we can use event.Props.Get
2024-06-21 18:38:41 +00:00
// notCompletedTODO
if event.Props["COMPLETED"] == nil {
if event.Props["STATUS"] == nil {
2024-06-21 18:38:41 +00:00
notCompletedTODO = true
} else {
if event.Props["STATUS"][0].Value != "COMPLETED" {
2024-06-21 18:38:41 +00:00
notCompletedTODO = true
2024-06-19 19:27:13 +00:00
}
}
2024-06-21 18:38:41 +00:00
}
// withTodayDate
if event.Props["DUE"] != nil {
// withDate = true
2024-06-21 18:38:41 +00:00
// fromToday
if strings.HasPrefix(event.Props["DUE"][0].Value, date.Format("20060102")) {
2024-06-21 18:38:41 +00:00
fromToday = true
}
}
// if notCompletedTODO && withDate && fromToday {
if notCompletedTODO && fromToday {
2024-06-21 18:38:41 +00:00
tmpTODO := event
if event.Props["SUMMARY"] == nil {
tmpTODO.Props.SetText(ical.PropSummary, "<EMPTY>")
2024-06-19 19:27:13 +00:00
}
if event.Props["DESCRIPTION"] == nil {
tmpTODO.Props.SetText(ical.PropDescription, "")
2024-06-21 18:38:41 +00:00
}
2024-06-21 18:38:41 +00:00
// var todoTime string
// due := (*event).Props["DUE"][0].Value
// index := strings.Index(due, "T")
// if index != -1 {
// str := due[index+1:]
// todoTime = str[:2] + ":" + str[2:4]
//
// tmpTODO.Time = todoTime
// }
2024-06-19 19:27:13 +00:00
2024-06-21 18:38:41 +00:00
output = append(output, tmpTODO)
}
2024-06-19 19:27:13 +00:00
// }
}
}
2024-06-21 18:38:41 +00:00
// //TODO sort: time, priority (if no time)
// // it means, put DUE at first, other DUE;VALUE=DATE in priority order
// //TODO color: priority, add time icon if have time
// //TODO UID:7ed30f40-fce1-422c-be3b-0486dcfe8943
// //TODO RELATED-TO:7ed30f40-fce1-422c-be3b-0486dcfe8943 # subtask
// //TODO PRIORITY:1 #1-high, 5-mid, 9-low
//TODO repeat function???
2024-06-19 19:27:13 +00:00
//TODO on complete -repeat function
// RRULE:FREQ=WEEKLY;INTERVAL=1
//TODO if no repeat - mark as complted
// STATUS:COMPLETED
// COMPLETED:20240421T065323Z
// PERCENT-COMPLETE:100
//TODO support notifcations/alarms???
// BEGIN:VTODO
// ...
// BEGIN:VALARM
// TRIGGER;RELATED=END:-PT15M
// ACTION:DISPLAY
// DESCRIPTION:Default Tasks.org description
// END:VALARM
// END:VTODO
// fmt.Println(output) //TODO rm me
// time.Sleep(3*time.Second)//TODO rm me
2024-06-21 18:38:41 +00:00
return output, nil
2024-06-19 19:27:13 +00:00
}
// type todoInterface struct {
// name string
// description string
// priority int
// dueTime time.Time
// name string
// }
func CreateTodo(name,description string, priority int, dueTime time.Time) (event ical.Event,err error) {
uid, err := uuid.NewUUID()
if err != nil {return}
event = *ical.NewEvent()
event.Props.SetText(ical.PropUID, uid.String())
event.Props.SetText(ical.PropSummary, name)
event.Props.SetText(ical.PropDescription, description)
if !dueTime.IsZero() {event.Props.SetDateTime(ical.PropDateTimeEnd, dueTime)} // 'zero' time is `time.Time{}`
switch priority {
case 0: //No priority
event.Props.SetText(ical.PropPriority, "0")
case 1: //Light
event.Props.SetText(ical.PropPriority, "1")
case 2: //Medium
event.Props.SetText(ical.PropPriority, "5")
case 3: //Urgent
event.Props.SetText(ical.PropPriority, "9")
default:
err = errors.New("Wrong priority, expecting 0-3")
return
}
event.Props.SetDateTime(ical.PropDateTimeStamp, time.Now().UTC())
event.Props.SetDateTime(ical.PropCreated, time.Now().UTC()) //TODO if it don't exist already (in case if we edit todo)
event.Props.SetDateTime(ical.PropLastModified, time.Now().UTC())
return
}
func (m model) UploadTodo(event ical.Event) (err error) {
// event.Props.SetDateTime(ical.PropDateTimeStart, startDateTime)
//TODO PropPriority = "PRIORITY"
// TODO Alarm component properties
// PropAction = "ACTION"
// PropRepeat = "REPEAT"
// PropTrigger = "TRIGGER"}
calendar, err := client.GetCalendarObject(ctx, m.Creds.CalendarPath)
if err != nil {return}
calendar.Data.Component.Children = append(calendar.Data.Component.Children, event.Component)
var buf strings.Builder
encoder := ical.NewEncoder(&buf)
err = encoder.Encode(calendar.Data)
if err != nil {return}
_, err = client.PutCalendarObject(ctx, m.Creds.CalendarPath, calendar.Data)
if err != nil {return}
return nil
}
func (m model) DelTodo(todo ical.Event) (err error) {
delUID,err := todo.Props.Get(ical.PropUID).Text()
if err != nil {return}
calendar, err := client.GetCalendarObject(ctx, m.Creds.CalendarPath)
if err != nil {return}
var newEvents []*ical.Component
for _, component := range calendar.Data.Component.Children {
if component.Name == ical.CompEvent {
var uid string
uid, err = component.Props.Text(ical.PropUID)
if err != nil {return}
if uid != delUID {
newEvents = append(newEvents, component)
}
}
}
calendar.Data.Component.Children = newEvents
var buf strings.Builder
encoder := ical.NewEncoder(&buf)
err = encoder.Encode(calendar.Data)
if err != nil {return}
_, err = client.PutCalendarObject(ctx, m.Creds.CalendarPath, calendar.Data)
if err != nil {return}
return nil
}
func (m model) EditTodo(todo ical.Event) (err error) {
//TODO is there proper edit function ???
err = m.DelTodo(todo)
if err != nil {return}
err = m.UploadTodo(todo)
if err != nil {
//TODO panic, we deleted but cant upload event. Need to save somehow. Or at least write debug string with parameters so we can re-add manually in that case
return
}
return nil
}
//TODO
func (m model) CompleteTodo(todo ical.Event) (err error) {
//TODO if it repitable - Repeate, otherwise complete it
err = m.EditTodo(todo)
if err != nil {return}
return nil
}