227 lines
6.0 KiB
Go
227 lines
6.0 KiB
Go
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 := `<?xml version="1.0" encoding="UTF-8"?>
|
|
<d:propfind xmlns:d="DAV:" xmlns:cs="urn:ietf:params:xml:ns:caldav" xmlns:apple="http://apple.com/ns/ical/">
|
|
<d:prop>
|
|
<d:displayname/>
|
|
<apple:calendar-color/>
|
|
<d:resourcetype/>
|
|
</d:prop>
|
|
</d:propfind>`
|
|
|
|
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(`<?xml version="1.0" encoding="UTF-8"?>
|
|
<c:calendar-query xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:caldav">
|
|
<d:prop>
|
|
<d:getetag/>
|
|
<c:calendar-data/>
|
|
</d:prop>
|
|
<c:filter>
|
|
<c:comp-filter name="VCALENDAR">
|
|
<c:comp-filter name="VEVENT">
|
|
<c:time-range start="%s" end="%s"/>
|
|
</c:comp-filter>
|
|
</c:comp-filter>
|
|
</c:filter>
|
|
</c:calendar-query>`, 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"`
|
|
}
|