From 5b78cdc104961f8cbd870513dee75dd823c6e4c6 Mon Sep 17 00:00:00 2001 From: Simon Ser Date: Wed, 5 Feb 2020 18:08:00 +0100 Subject: plugins/caldav: new plugin For now it can only list events for the current month. References: https://todo.sr.ht/~sircmpwn/koushin/60 --- plugins/caldav/caldav.go | 61 +++++++++++++++++++++++++++++++ plugins/caldav/plugin.go | 71 +++++++++++++++++++++++++++++++++++++ plugins/caldav/public/calendar.html | 25 +++++++++++++ plugins/caldav/routes.go | 66 ++++++++++++++++++++++++++++++++++ 4 files changed, 223 insertions(+) create mode 100644 plugins/caldav/caldav.go create mode 100644 plugins/caldav/plugin.go create mode 100644 plugins/caldav/public/calendar.html create mode 100644 plugins/caldav/routes.go (limited to 'plugins/caldav') diff --git a/plugins/caldav/caldav.go b/plugins/caldav/caldav.go new file mode 100644 index 0000000..1c043e9 --- /dev/null +++ b/plugins/caldav/caldav.go @@ -0,0 +1,61 @@ +package koushincaldav + +import ( + "fmt" + "net/http" + "net/url" + + "git.sr.ht/~emersion/koushin" + "github.com/emersion/go-webdav/caldav" +) + +var errNoCalendar = fmt.Errorf("caldav: no calendar found") + +type authRoundTripper struct { + upstream http.RoundTripper + session *koushin.Session +} + +func (rt *authRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { + rt.session.SetHTTPBasicAuth(req) + return rt.upstream.RoundTrip(req) +} + +func newClient(u *url.URL, session *koushin.Session) (*caldav.Client, error) { + rt := authRoundTripper{ + upstream: http.DefaultTransport, + session: session, + } + c, err := caldav.NewClient(&http.Client{Transport: &rt}, u.String()) + if err != nil { + return nil, fmt.Errorf("failed to create CalDAV client: %v", err) + } + + return c, nil +} + +func getCalendar(u *url.URL, session *koushin.Session) (*caldav.Client, *caldav.Calendar, error) { + c, err := newClient(u, session) + if err != nil { + return nil, nil, err + } + + principal, err := c.FindCurrentUserPrincipal() + if err != nil { + return nil, nil, fmt.Errorf("failed to query CalDAV principal: %v", err) + } + + calendarHomeSet, err := c.FindCalendarHomeSet(principal) + if err != nil { + return nil, nil, fmt.Errorf("failed to query CalDAV calendar home set: %v", err) + } + + calendars, err := c.FindCalendars(calendarHomeSet) + if err != nil { + return nil, nil, fmt.Errorf("failed to find calendars: %v", err) + } + if len(calendars) == 0 { + return nil, nil, errNoCalendar + } + return c, &calendars[0], nil +} diff --git a/plugins/caldav/plugin.go b/plugins/caldav/plugin.go new file mode 100644 index 0000000..1915646 --- /dev/null +++ b/plugins/caldav/plugin.go @@ -0,0 +1,71 @@ +package koushincaldav + +import ( + "fmt" + "net/http" + "net/url" + + "git.sr.ht/~emersion/koushin" +) + +func sanityCheckURL(u *url.URL) error { + req, err := http.NewRequest(http.MethodOptions, u.String(), nil) + if err != nil { + return err + } + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return err + } + resp.Body.Close() + + // Servers might require authentication to perform an OPTIONS request + if resp.StatusCode/100 != 2 && resp.StatusCode != http.StatusUnauthorized { + return fmt.Errorf("HTTP request failed: %v %v", resp.StatusCode, resp.Status) + } + return nil +} + +func newPlugin(srv *koushin.Server) (koushin.Plugin, error) { + u, err := srv.Upstream("caldavs", "caldav+insecure", "https", "http+insecure") + if _, ok := err.(*koushin.NoUpstreamError); ok { + return nil, nil + } else if err != nil { + return nil, fmt.Errorf("caldav: failed to parse upstream caldav server: %v", err) + } + switch u.Scheme { + case "caldavs": + u.Scheme = "https" + case "caldav+insecure", "http+insecure": + u.Scheme = "http" + } + if u.Scheme == "" { + return nil, fmt.Errorf("caldav: discovery not yet implemented") // TODO + } + + if err := sanityCheckURL(u); err != nil { + return nil, fmt.Errorf("caldav: failed to connect to CalDAV server %q: %v", u, err) + } + + srv.Logger().Printf("Configured upstream CalDAV server: %v", u) + + p := koushin.GoPlugin{Name: "caldav"} + + registerRoutes(&p, u) + + return p.Plugin(), nil +} + +func init() { + koushin.RegisterPluginLoader(func(s *koushin.Server) ([]koushin.Plugin, error) { + p, err := newPlugin(s) + if err != nil { + return nil, err + } + if p == nil { + return nil, nil + } + return []koushin.Plugin{p}, err + }) +} diff --git a/plugins/caldav/public/calendar.html b/plugins/caldav/public/calendar.html new file mode 100644 index 0000000..6011f9e --- /dev/null +++ b/plugins/caldav/public/calendar.html @@ -0,0 +1,25 @@ +{{template "head.html"}} + +

koushin

+ +

+ Back +

+ +

Calendar: {{.Calendar.Name}}

+ +{{if .Events}} + +{{else}} +

No events.

+{{end}} + +{{template "foot.html"}} diff --git a/plugins/caldav/routes.go b/plugins/caldav/routes.go new file mode 100644 index 0000000..acc879f --- /dev/null +++ b/plugins/caldav/routes.go @@ -0,0 +1,66 @@ +package koushincaldav + +import ( + "fmt" + "net/http" + "net/url" + "time" + + "git.sr.ht/~emersion/koushin" + "github.com/emersion/go-webdav/caldav" +) + +type CalendarRenderData struct { + koushin.BaseRenderData + Calendar *caldav.Calendar + Events []caldav.CalendarObject +} + +func registerRoutes(p *koushin.GoPlugin, u *url.URL) { + p.GET("/calendar", func(ctx *koushin.Context) error { + // TODO: multi-calendar support + c, calendar, err := getCalendar(u, ctx.Session) + if err != nil { + return err + } + + now := time.Now() + start := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location()) + end := start.AddDate(0, 1, 0) + + query := caldav.CalendarQuery{ + CompRequest: caldav.CalendarCompRequest{ + Name: "VCALENDAR", + Props: []string{"VERSION"}, + Comps: []caldav.CalendarCompRequest{{ + Name: "VEVENT", + Props: []string{ + "SUMMARY", + "UID", + "DTSTART", + "DTEND", + "DURATION", + }, + }}, + }, + CompFilter: caldav.CompFilter{ + Name: "VCALENDAR", + Comps: []caldav.CompFilter{{ + Name: "VEVENT", + Start: start, + End: end, + }}, + }, + } + events, err := c.QueryCalendar(calendar.Path, &query) + if err != nil { + return fmt.Errorf("failed to query calendar: %v", err) + } + + return ctx.Render(http.StatusOK, "calendar.html", &CalendarRenderData{ + BaseRenderData: *koushin.NewBaseRenderData(ctx), + Calendar: calendar, + Events: events, + }) + }) +} -- cgit v1.2.3