package main import ( "context" // "fmt" webdav "github.com/emersion/go-webdav" "github.com/emersion/go-webdav/caldav" "github.com/emersion/go-ical" "strings" "github.com/google/uuid" "time" "errors" "net/http" // "net/url" // "fmt" ) // 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 // } //TODO Is it safe to create global variables? var clientWebDAV *webdav.Client var client *caldav.Client // clientCalDAV // var calendarObjects []caldav.CalendarObject var ctx = context.Background() // var authSession caldav.Client // clientCalDAV func InitDAVclients(url,user,pass string) error { var err error authSession := webdav.HTTPClientWithBasicAuth(nil, user,pass) clientWebDAV, err = webdav.NewClient(authSession, url) if err != nil { // Handle error return err } client, err = caldav.NewClient(authSession, url) 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) if err != nil { // Handle error return err } return nil } func GetCalendars() ([]caldav.Calendar, error) { principal, err := clientWebDAV.FindCurrentUserPrincipal(ctx) if err != nil { // Handle error return nil, err } // fmt.Println("principal: ",principal) TODO log // Find the calendar home set calendarHomeSet, err := client.FindCalendarHomeSet(ctx, principal) if err != nil { return nil, err } // Find calendars in the calendar home set calendars, err := client.FindCalendars(ctx, calendarHomeSet) if err != nil { return nil, err } return calendars, nil } 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? date := time.Now() 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), // dateEnd:= time.Date(date.Year(), date.Month(), date.Day(), 23, 59, 59, 0, date.Location()) //date +1 day dateEnd := date.AddDate(0, 1, 0) //TODO we hard limit pulling events only for this month, to prevent getting tooo much events calQuery := caldav.CalendarQuery{ CompRequest: caldav.CalendarCompRequest{ Name: "VCALENDAR", Comps: []caldav.CalendarCompRequest{{ Name: "VTODO", Props: []string{ "UID", "SUMMARY", "COMPLETED", "DESCRIPTION", "DUE", "PRIORITY", "RELATED-TO", }, }, }, }, CompFilter: caldav.CompFilter{ Name: "VCALENDAR", Comps: []caldav.CompFilter{ { Name: "VTODO", Start: dateStart, End: dateEnd, // FYI: server-side filtering is not supported for Nextcloud }, }, }, } calendarObjects, err = client.QueryCalendar(ctx, calendarPath, &calQuery) if err != nil { return nil, err } return calendarObjects, nil } // 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 = "" // } // 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 for _, calObj := range calObjs { // 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 // 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 { tmpTODO := event if event.Props["SUMMARY"] == nil { tmpTODO.Props.SetText(ical.PropSummary, "") } if event.Props["DESCRIPTION"] == nil { tmpTODO.Props.SetText(ical.PropDescription, "") } // 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 on complete -repeat function //TODO repeate - RRULE:FREQ=DAILY;INTERVAL=1 // 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 return output, nil } type TodoInterface struct { name string description string priority int dueTime time.Time alarmOffset string repeat string //TODO repeat //TODO subtasks } func CreateTodo(info TodoInterface) (event ical.Event,err error) { uid, err := uuid.NewUUID() if err != nil {return} event = *ical.NewEvent() event.Name = ical.CompToDo //VTODO event.Props.SetText(ical.PropUID, uid.String()) event.Props.SetText(ical.PropSummary, info.name) event.Props.SetText(ical.PropDescription, info.description) if !info.dueTime.IsZero() {event.Props.SetDateTime(ical.PropDue, info.dueTime)} // 'zero' time is `time.Time{}` //TODO add alarm switch info.priority { case 0: //No priority event.Props.SetText(ical.PropPriority, "0") case 1: //Light event.Props.SetText(ical.PropPriority, "9") case 2: //Medium event.Props.SetText(ical.PropPriority, "5") case 3: //Urgent event.Props.SetText(ical.PropPriority, "1") default: err = errors.New("Wrong priority, expecting 0-3") return } //TODO repeat - RRULE:FREQ=DAILY;INTERVAL=1 if info.alarmOffset != "" { // alarmComponent := ical.Component{Name:ical.CompAlarm} alarmComponent := ical.NewComponent(ical.CompAlarm) alarmComponent.Props.SetText(ical.PropAction,"DISPLAY") // alarmComponent.Props.Add(&ical.Prop{Name:ical.PropAction,Value:"DISPLAY",}) alarmComponent.Props.Add(&ical.Prop{Name:ical.PropDescription,Value:"Default Alarm Tempus description",}) alarmComponent.Props.SetText(ical.PropDescription,"Default Alarm Tempus description") // ACTION:DISPLAY // DESCRIPTION:Default Tasks.org description var value string if info.alarmOffset == "0" { // TRIGGER;RELATED=END:PT0S value = "PT0S" } if strings.HasSuffix(info.alarmOffset,"h") { offset,_ := strings.CutSuffix(info.alarmOffset,"h") value="-PT" + offset + "H" // TRIGGER;RELATED=END:-PT1H } //TODO stop next if if strings.HasSuffix(info.alarmOffset,"m") { offset,_ := strings.CutSuffix(info.alarmOffset,"m") value="-PT" + offset + "M" // TRIGGER;RELATED=END:-PT10M } if strings.HasSuffix(info.alarmOffset,"d") { offset,_ := strings.CutSuffix(info.alarmOffset,"d") value="-P" + offset + "D" // TRIGGER;RELATED=END:-P1D } alarmComponent.Props.SetText("TRIGGER;RELATED=END",value) event.Children = append(event.Children,alarmComponent) } 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()) //TODO add a function to verify event (exist in ical lib) return } func (m model) UploadTodo(event ical.Event) (err error) { // event.Props.SetDateTime(ical.PropDateTimeStart, startDateTime) // TODO Alarm component properties // PropAction = "ACTION" // PropRepeat = "REPEAT" // PropTrigger = "TRIGGER"} // calendar, err := client.GetCalendarObject(ctx, m.Creds.CalendarPath) //makes error on nextcloud // if err != nil {return err} calendar := ical.NewCalendar() calendar.Props.SetText(ical.PropProductID, "+//Casual//Tempus//EN") calendar.Props.SetText(ical.PropVersion, "2.0") calendar.Component.Children = append(calendar.Component.Children, event.Component) todoGUID,err := event.Props.Get(ical.PropUID).Text() if err != nil {return err} //TODO check GUID uniq and regenerate if needed var buf strings.Builder encoder := ical.NewEncoder(&buf) err = encoder.Encode(calendar) if err != nil {return err} _, err = client.PutCalendarObject(ctx, m.Creds.CalendarPath+todoGUID+".isc", calendar) if err != nil {return err} return nil } func (m model) DelTodo(delUID string) (err error) { // 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+delUID+".isc") // 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} // req := http.Request{ // Method: "DELETE", // URL: url.Parse(m.Creds.URL + m.Creds.CalendarPath + delUID + ".isc"), // // } client := &http.Client{} parts := strings.Split(m.Creds.URL, "/") baseURL := parts[0]+"//"+parts[2] // Create request req, err := http.NewRequest("DELETE", baseURL + m.Creds.CalendarPath + delUID + ".isc", nil) if err != nil { // fmt.Println(err) return } req.SetBasicAuth(m.Creds.Username, m.Creds.Password) // Fetch Request resp, err := client.Do(req) if err != nil { // fmt.Println(err) return } defer resp.Body.Close() // Read Response Body // respBody, err := ioutil.ReadAll(resp.Body) // if err != nil { // // fmt.Println(err) // return // } if resp.Status != "204 No Content" {return errors.New("Can't delete, response status: "+resp.Status+".")} // Display Results // fmt.Println("response Status : ", resp.Status) // fmt.Println("response Headers : ", resp.Header) // fmt.Println("response Body : ", string(respBody)) return nil } func (m model) EditTodo(todo ical.Event) (err error) { //TODO is there proper edit function ??? uid,err := todo.Props.Get(ical.PropUID).Text() if err != nil {return} err = m.DelTodo(uid) 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 }