package nextcloud import ( "context" "encoding/xml" "fmt" "io" "strings" "time" ) type Calendar struct { ID string `json:"id"` DisplayName string `json:"display_name"` Color string `json:"color"` Path string `json:"path"` } type Event struct { UID string `json:"uid"` Summary string `json:"summary"` Description string `json:"description"` Location string `json:"location"` Start string `json:"start"` End string `json:"end"` AllDay bool `json:"all_day"` RawICS string `json:"raw_ics,omitempty"` } func (c *Client) ListCalendars(ctx context.Context, userID string) ([]Calendar, error) { path := fmt.Sprintf("/remote.php/dav/calendars/%s/", userID) body := ` ` resp, err := c.DoAsUser(ctx, "PROPFIND", path, strings.NewReader(body), userID, map[string]string{ "Depth": "1", "Content-Type": "application/xml", }) if err != nil { return nil, err } defer resp.Body.Close() return parseCalendarList(resp.Body, path) } func (c *Client) ListEvents(ctx context.Context, userID, calendarPath string, from, to time.Time) ([]Event, error) { body := fmt.Sprintf(` `, from.Format("20060102T150405Z"), to.Format("20060102T150405Z")) resp, err := c.DoAsUser(ctx, "REPORT", calendarPath, strings.NewReader(body), userID, map[string]string{ "Depth": "1", "Content-Type": "application/xml", }) if err != nil { return nil, err } defer resp.Body.Close() return parseEventList(resp.Body) } func (c *Client) CreateEvent(ctx context.Context, userID, calendarPath string, event *Event) error { ics := buildICS(event) uid := event.UID if uid == "" { uid = fmt.Sprintf("%d@ulti", time.Now().UnixNano()) } eventPath := fmt.Sprintf("%s%s.ics", calendarPath, uid) resp, err := c.DoAsUser(ctx, "PUT", eventPath, strings.NewReader(ics), userID, map[string]string{ "Content-Type": "text/calendar; charset=utf-8", }) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode != 201 && resp.StatusCode != 204 { return fmt.Errorf("create event failed: %d", resp.StatusCode) } return nil } func (c *Client) DeleteEvent(ctx context.Context, userID, eventPath string) error { resp, err := c.DoAsUser(ctx, "DELETE", eventPath, nil, userID, nil) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode != 204 { return fmt.Errorf("delete event failed: %d", resp.StatusCode) } return nil } func buildICS(event *Event) string { var b strings.Builder b.WriteString("BEGIN:VCALENDAR\r\n") b.WriteString("VERSION:2.0\r\n") b.WriteString("PRODID:-//Ulti Suite//EN\r\n") b.WriteString("BEGIN:VEVENT\r\n") if event.UID != "" { b.WriteString(fmt.Sprintf("UID:%s\r\n", event.UID)) } b.WriteString(fmt.Sprintf("SUMMARY:%s\r\n", event.Summary)) if event.Description != "" { b.WriteString(fmt.Sprintf("DESCRIPTION:%s\r\n", event.Description)) } if event.Location != "" { b.WriteString(fmt.Sprintf("LOCATION:%s\r\n", event.Location)) } b.WriteString(fmt.Sprintf("DTSTART:%s\r\n", event.Start)) b.WriteString(fmt.Sprintf("DTEND:%s\r\n", event.End)) b.WriteString(fmt.Sprintf("DTSTAMP:%s\r\n", time.Now().UTC().Format("20060102T150405Z"))) b.WriteString("END:VEVENT\r\n") b.WriteString("END:VCALENDAR\r\n") return b.String() } func parseCalendarList(body io.Reader, basePath string) ([]Calendar, error) { var ms multistatus if err := xml.NewDecoder(body).Decode(&ms); err != nil { return nil, err } calendars := make([]Calendar, 0) for _, r := range ms.Responses { if r.Href == basePath { continue } name := r.Propstat.Prop.DisplayName if name == "" { continue } calendars = append(calendars, Calendar{ ID: strings.TrimSuffix(strings.TrimPrefix(r.Href, basePath), "/"), DisplayName: name, Color: r.Propstat.Prop.CalendarColor, Path: r.Href, }) } return calendars, nil } func parseEventList(body io.Reader) ([]Event, error) { var ms calMultistatus if err := xml.NewDecoder(body).Decode(&ms); err != nil { return nil, err } events := make([]Event, 0, len(ms.Responses)) for _, r := range ms.Responses { ics := r.Propstat.Prop.CalendarData event := parseICS(ics) event.RawICS = ics events = append(events, event) } return events, nil } func parseICS(ics string) Event { var e Event for _, line := range strings.Split(ics, "\n") { line = strings.TrimSpace(line) switch { case strings.HasPrefix(line, "UID:"): e.UID = strings.TrimPrefix(line, "UID:") case strings.HasPrefix(line, "SUMMARY:"): e.Summary = strings.TrimPrefix(line, "SUMMARY:") case strings.HasPrefix(line, "DESCRIPTION:"): e.Description = strings.TrimPrefix(line, "DESCRIPTION:") case strings.HasPrefix(line, "LOCATION:"): e.Location = strings.TrimPrefix(line, "LOCATION:") case strings.HasPrefix(line, "DTSTART"): e.Start = extractValue(line) case strings.HasPrefix(line, "DTEND"): e.End = extractValue(line) } } return e } func extractValue(line string) string { if idx := strings.LastIndex(line, ":"); idx >= 0 { return line[idx+1:] } return line } type calMultistatus struct { XMLName xml.Name `xml:"multistatus"` Responses []calResponse `xml:"response"` } type calResponse struct { Href string `xml:"href"` Propstat calPropstat `xml:"propstat"` } type calPropstat struct { Prop calProp `xml:"prop"` } type calProp struct { ETag string `xml:"getetag"` CalendarData string `xml:"calendar-data"` }