diff --git a/src/calanonsync/calanonsync.go b/src/calanonsync/calanonsync.go index c64a771..f5f67f0 100644 --- a/src/calanonsync/calanonsync.go +++ b/src/calanonsync/calanonsync.go @@ -169,6 +169,17 @@ func main() { for _, item := range items { fmt.Printf("%#v\n", item) } + + c := NewCalDAV(s.CalDAV) + calDavItems, err := c.GetEvents() + if err != nil { + log.Println(err) + return + } + for _, item := range calDavItems { + fmt.Printf("%s\n UID: %s\n Summary: %s\n Start: %s\n End: %s\n", + item.HRef, item.UID(), item.Summary(), item.Start(), item.End()) + } } const folderIdRequest = ` diff --git a/src/calanonsync/caldav.go b/src/calanonsync/caldav.go new file mode 100644 index 0000000..aa5b65c --- /dev/null +++ b/src/calanonsync/caldav.go @@ -0,0 +1,176 @@ +package main + +import ( + "bufio" + "encoding/xml" + "errors" + "io" + "log" + "net/http" + "strings" + "time" +) + +const ICAL_TIME = "20060102T150405" +const ICAL_DATE = "20060102" + +type CalDAV struct { + ServerSettings + httpClient *http.Client +} + +type ICal struct { + data []string + eventData []string // sub slice +} + +func (i ICal) Summary() string { + for _, s := range i.eventData { + if strings.HasPrefix(s, "SUMMARY:") { + return s[len("SUMMARY:"):] + } + } + return "" +} + +func (i ICal) UID() string { + for _, s := range i.eventData { + if strings.HasPrefix(s, "UID:") { + return s[len("UID:"):] + } + } + return "" +} + +func (i ICal) getTimeField(f string) time.Time { + for _, s := range i.eventData { + if strings.HasPrefix(s, f) { + st := s[strings.LastIndex(s, ":")+1:] + // BEWARE, we don't parse possible ICal timezones (yet). If it is not + // a UTC time, we simply assume local time. + zone := time.Local + if st[len(st)-1:] == "Z" { + st = st[:len(st)-1] + zone = time.UTC + } + t, err := time.ParseInLocation(ICAL_TIME, st, zone) + if err != nil { + log.Printf("Cannot decode %s '%s': %s", f, st, err) + } + return t + } + } + return time.Time{} +} + +func (i ICal) Start() time.Time { + return i.getTimeField("DTSTART") +} + +func (i ICal) End() time.Time { + return i.getTimeField("DTEND") +} + +func (i *ICal) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { + s := "" + err := d.DecodeElement(&s, &start) + if err != nil { + return err + } + ical, err := ParseICal(strings.NewReader(s)) + if err != nil { + return err + } + + *i = *ical + return nil +} + +func ParseICal(r io.Reader) (*ICal, error) { + var data []string + scanner := bufio.NewScanner(r) + eventStart := -1 + eventEnd := -1 + i := 0 + for scanner.Scan() { + line := scanner.Text() + data = append(data, line) + + if eventStart == -1 && line == "BEGIN:VEVENT" { + eventStart = i + } + if eventEnd == -1 && line == "END:VEVENT" { + eventEnd = i + } + + i++ + } + err := scanner.Err() + if err != nil { + return nil, err + } + + // If we didn't find a start or an end, abort. + if eventStart < 0 || eventEnd <= eventStart { + return nil, errors.New("calendar item without event") + } + + return &ICal{ + data: data, + eventData: data[eventStart+1 : eventEnd], + }, err +} + +func NewCalDAV(settings ServerSettings) *CalDAV { + return &CalDAV{ + httpClient: http.DefaultClient, + ServerSettings: settings, + } +} + +type CalDAVItem struct { + HRef string + ICal +} + +func (c *CalDAV) GetEvents() ([]CalDAVItem, error) { + req, err := http.NewRequest("PROPFIND", c.URL, strings.NewReader(``)) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", "text/xml") + req.SetBasicAuth(c.Username, c.Password) + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return nil, err + } + + defer resp.Body.Close() + ms := &MultiStatus{} + err = xml.NewDecoder(resp.Body).Decode(ms) + if err != nil { + return nil, err + } + + result := make([]CalDAVItem, 0, len(ms.Response)) + for _, p := range ms.Response { + item := CalDAVItem{p.HRef, p.PropStat.Prop.CalendarData} + result = append(result, item) + } + + return result, nil +} + +type MultiStatus struct { + Response []PropFindResponse `xml:"response"` +} + +type PropFindResponse struct { + HRef string `xml:"href"` + PropStat struct { + Prop struct { + CalendarData ICal `xml:"calendar-data"` + } `xml:"prop"` + } `xml:"propstat"` +}