commit b7e9941ecde84a7ef0e2142d0a34c49f5e821f4d
parent 3de55c280733b19053918faf05a975a6e7b48599
Author: Andreas Gruhler <andreas.gruhler@adfinis.com>
Date: Thu, 13 Jun 2024 21:07:47 +0200
feat(discovery): add HIVEDAV_CALDAV_HOST
Diffstat:
6 files changed, 90 insertions(+), 38 deletions(-)
diff --git a/README.md b/README.md
@@ -49,6 +49,7 @@ The application is configurable through environment variables or the
# Optional
#HIVEDAV_LISTEN_ADDRESS=127.0.0.1
#HIVEDAV_LISTEN_PORT=3737
+# only useful for discovery of the calendar when HIVEDAV_CALDAV_HOST is set
#HIVEDAV_CALENDAR=0
#HIVEDAV_HORIZON=1
#HIVEDAV_BOOKING_SUBJ="HiveDAV Booking"
@@ -58,7 +59,10 @@ The application is configurable through environment variables or the
# Required
HIVEDAV_HOST=hivedav.example.com
+# an absolute uri to a caldav calendar, mutually exclusive with HIVEDAV_CALDAV_HOST
HIVEDAV_CALDAV_URI=
+# the host for caldav discovery might have a port, mutually exclusive with HIVEDAV_CALDAV_URI
+HIVEDAV_CALDAV_HOST=
HIVEDAV_CALDAV_USER=
# make sure to quote in single quotes to properly esacpe special chars
HIVEDAV_CALDAV_PASSWORD=''
@@ -71,6 +75,11 @@ HIVEDAV_SMTP_PASSWORD=''
There is an example config file provided in `./app.env.example`.
+* `HIVEDAV_CALDAV_URI` is an absolute uri to a calendar resource on a caldav
+ server. No discovery is performed. You either have to define
+ `HIVEDAV_CALDAV_URI` or `HIVEDAV_CALDAV_HOST`.
+* With `HIVEDAV_CALDAV_HOST`, HiveDAV performs a discovery of the calendar uri
+ using `HIVEDAV_CALENDAR`.
* The `HIVEDAV_LISTEN_*` variables specify the http server `ADDRESS` and `PORT`
(bind address and port)
* The `HIVEDAV_CALENDAR` is a number that points to the index of the calendar
@@ -148,6 +157,8 @@ application work with as many remote servers as possible, all events from the
remote calendar are fetched during an update and the availability data is
stored in a local database for further processing.
+For more information refer to [`./docs/CALDAV.md`](./docs/CALDAV.md)
+
## Availability Query
- s: start time
diff --git a/app.env.example b/app.env.example
@@ -1,7 +1,11 @@
#HIVEDAV_LISTEN_ADDRESS=127.0.0.1
#HIVEDAV_LISTEN_PORT=3737
#HIVEDAV_HOST=hivedav.example.com
+# an absolute uri to a caldav calendar, mutually exclusive with HIVEDAV_CALDAV_HOST
#HIVEDAV_CALDAV_URI=
+# the host for caldav discovery might have a port, mutually exclusive with HIVEDAV_CALDAV_URI
+#HIVEDAV_CALDAV_HOST=
+# only useful for discovery of the calendar when HIVEDAV_CALDAV_HOST is set
#HIVEDAV_CALENDAR=0
#HIVEDAV_HORIZON=1
#HIVEDAV_CALDAV_USER=
diff --git a/caldav/caldav.go b/caldav/caldav.go
@@ -36,18 +36,19 @@ type CalData struct {
Data string `xml:"propstat>prop>calendar-data"`
}
-func UserPrincipal(caldavUri string, user string, pass string) (string, error) {
+func UserPrincipal(caldavHost string, user string, pass string) (string, error) {
payload := []byte(`<d:propfind xmlns:d="DAV:">
<d:prop>
<d:current-user-principal />
</d:prop>
</d:propfind>`)
reader := bytes.NewReader(payload)
- req, err := http.NewRequest("PROPFIND", caldavUri, reader)
+ req, err := http.NewRequest("PROPFIND", caldavHost, reader)
if err != nil {
return "", err
}
req.Header.Set("Depth", "0")
+ req.Header.Set("Content-Type", "text/xml")
req.SetBasicAuth(user, pass)
res, err := http.DefaultClient.Do(req)
@@ -69,20 +70,21 @@ func UserPrincipal(caldavUri string, user string, pass string) (string, error) {
return xmlResponse.UserPrincipal, nil
}
-func CalendarHome(caldavUri string, user string, pass string, userPrincipal string, calendar int) (string, error) {
+func CalendarHome(caldavHost string, user string, pass string, userPrincipal string, calendar int) (string, error) {
payload := []byte(`<d:propfind xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:caldav">
<d:prop>
<c:calendar-home-set />
</d:prop>
</d:propfind>`)
reader := bytes.NewReader(payload)
- principalUri := fmt.Sprintf("%s%s", caldavUri, userPrincipal)
+ principalPath := fmt.Sprintf("%s%s", caldavHost, userPrincipal)
- req, err := http.NewRequest("PROPFIND", principalUri, reader)
+ req, err := http.NewRequest("PROPFIND", principalPath, reader)
if err != nil {
return "", err
}
req.Header.Set("Depth", "0")
+ req.Header.Set("Content-Type", "text/xml")
req.SetBasicAuth(user, pass)
res, err := http.DefaultClient.Do(req)
@@ -104,7 +106,7 @@ func CalendarHome(caldavUri string, user string, pass string, userPrincipal stri
return xmlResponse.HomeSet, nil
}
-func CalendarFromHomeSet(caldavUri string, user string, pass string, homeSetUri string, calendar int) (calendarUri string, err error) {
+func CalendarFromHomeSet(caldavHost string, user string, pass string, homeSetPath string, calendar int) (calendarPath string, err error) {
payload := []byte(`<d:propfind xmlns:d="DAV:" xmlns:cs="http://calendarserver.org/ns/" xmlns:c="urn:ietf:params:xml:ns:caldav">
<d:prop>
<d:resourcetype />
@@ -114,13 +116,14 @@ func CalendarFromHomeSet(caldavUri string, user string, pass string, homeSetUri
</d:prop>
</d:propfind>`)
reader := bytes.NewReader(payload)
- homeSetUrl := fmt.Sprintf("%s%s", caldavUri, homeSetUri)
+ homeSetUrl := fmt.Sprintf("%s%s", caldavHost, homeSetPath)
req, err := http.NewRequest("PROPFIND", homeSetUrl, reader)
if err != nil {
return "", err
}
req.Header.Set("Depth", "1")
+ req.Header.Set("Content-Type", "text/xml")
req.SetBasicAuth(user, pass)
res, err := http.DefaultClient.Do(req)
@@ -145,11 +148,17 @@ func CalendarFromHomeSet(caldavUri string, user string, pass string, homeSetUri
}
}()
- calendarUri = xmlResponse.Response[calendar].Href
- return calendarUri, nil
+ calendarPath = xmlResponse.Response[calendar].Href
+ return calendarPath, nil
}
-func GetAvailability(caldavUri string, user string, pass string, calendarUri string) (data []CalData, err error) {
+func GetAvailability(caldavHost string, user string, pass string, calendarPath string) (data []CalData, err error) {
+ // concatenant the host and the calendar path
+ calendarUrl := fmt.Sprintf("%s%s", caldavHost, calendarPath)
+ return GetAvailabilityByUrl(calendarUrl, user, pass)
+}
+
+func GetAvailabilityByUrl(calendarUrl string, user string, pass string) (data []CalData, err error) {
payload := []byte(`<c:calendar-query xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:caldav">
<d:prop>
<d:getetag />
@@ -162,13 +171,13 @@ func GetAvailability(caldavUri string, user string, pass string, calendarUri str
</c:filter>
</c:calendar-query>`)
reader := bytes.NewReader(payload)
- calendarUrl := fmt.Sprintf("%s%s", caldavUri, calendarUri)
req, err := http.NewRequest("REPORT", calendarUrl, reader)
if err != nil {
return data, err
}
req.Header.Set("Depth", "1")
+ req.Header.Set("Content-Type", "text/xml")
req.SetBasicAuth(user, pass)
res, err := http.DefaultClient.Do(req)
diff --git a/config/config.go b/config/config.go
@@ -10,6 +10,7 @@ type Config struct {
ListenAddress string `mapstructure:"HIVEDAV_LISTEN_ADDRESS"`
ListenPort int `mapstructure:"HIVEDAV_LISTEN_PORT"`
CaldavUri string `mapstructure:"HIVEDAV_CALDAV_URI"`
+ CaldavHost string `mapstructure:"HIVEDAV_CALDAV_HOST"`
Calendar int `mapstructure:"HIVEDAV_CALENDAR"`
Horizon int `mapstructure:"HIVEDAV_HORIZON"`
CaldavUser string `mapstructure:"HIVEDAV_CALDAV_USER"`
@@ -74,6 +75,9 @@ func (c *Config) LoadConfig(path string) (*Config, error) {
if viper.IsSet("CALDAV_URI") {
c.CaldavUri = viper.GetString("CALDAV_URI")
}
+ if viper.IsSet("CALDAV_HOST") {
+ c.CaldavHost = viper.GetString("CALDAV_HOST")
+ }
if viper.IsSet("CALENDAR") {
c.Calendar = viper.GetInt("CALENDAR")
}
diff --git a/main.go b/main.go
@@ -29,10 +29,11 @@ func main() {
log.Printf("HiveDAV %s, %s 🍯\n", hivedavVersion, conf.HiveDavHost)
log.Println("----------------------------------------------------")
- log.Printf("Cron interval:\t%d minutes\n", conf.RefreshInterval)
- log.Printf("Calendar horizon:\t%d year(s)\n", conf.Horizon)
- log.Printf("CalDAV server:\t%s\n", conf.CaldavUri)
- log.Printf("CalDAV user:\t%s\n", conf.CaldavUser)
+ log.Printf("Cron interval:\t\t%d minutes\n", conf.RefreshInterval)
+ log.Printf("Calendar horizon:\t\t%d year(s)\n", conf.Horizon)
+ log.Printf("CalDAV uri:\t\t\t%s\n", conf.CaldavUri)
+ log.Printf("CalDAV discovery host:\t%s\n", conf.CaldavHost)
+ log.Printf("CalDAV user:\t\t%s\n", conf.CaldavUser)
c := cron.New()
c.AddFunc(fmt.Sprintf("@every %dm", conf.RefreshInterval), func() { updateDatabase(server, c) })
@@ -48,36 +49,51 @@ func main() {
router.GET("/book/:dtstart", server.CubicleForm)
router.ServeFiles("/css/*filepath", http.Dir("./css"))
router.GET("/healthz", server.Healthz)
- log.Printf("Listening:\t\t%s:%d\n", conf.ListenAddress, conf.ListenPort)
+ log.Printf("Listening:\t\t\t%s:%d\n", conf.ListenAddress, conf.ListenPort)
log.Fatal(http.ListenAndServe(fmt.Sprintf("%s:%d", conf.ListenAddress, conf.ListenPort), router))
}
func updateDatabase(s *Server, c *cron.Cron) {
- // Find current user principal
- userPrincipal, err := caldav.UserPrincipal(s.config.CaldavUri, s.config.CaldavUser, s.config.CaldavPassword)
- if err != nil {
- log.Fatal("Error reading current user principal: ", err)
- }
- log.Printf("User principal:\t%s\n", userPrincipal)
+ var calData []caldav.CalData
+ var calendarPath string
+ var err error
+ // discover calendar based on HIVEDAV_CALENDAR when no CALDAV_URI is set
+ if s.config.CaldavUri == "" {
+ log.Println("----------------------------------------------------")
+ log.Printf("Discovering calendars on host %s...\n", s.config.CaldavHost)
+ // Find current user principal
+ userPrincipal, err := caldav.UserPrincipal(s.config.CaldavHost, s.config.CaldavUser, s.config.CaldavPassword)
+ if err != nil {
+ log.Fatal("Error reading current user principal: ", err)
+ }
+ log.Printf("User principal:\t\t%s\n", userPrincipal)
- // Find home set of current user principal
- homeSetUri, err := caldav.CalendarHome(s.config.CaldavUri, s.config.CaldavUser, s.config.CaldavPassword, userPrincipal, s.config.Calendar)
- if err != nil {
- log.Fatal("Error reading home set URI: ", err)
- }
- log.Printf("Calendar homeset:\t%s", homeSetUri)
+ // Find home set of current user principal
+ homeSetPath, err := caldav.CalendarHome(s.config.CaldavHost, s.config.CaldavUser, s.config.CaldavPassword, userPrincipal, s.config.Calendar)
+ if err != nil {
+ log.Fatal("Error reading home set URI: ", err)
+ }
+ log.Printf("Calendar homeset:\t\t%s", homeSetPath)
- // Find calendar URI for the calendar specified in the config
- calendarUri, err := caldav.CalendarFromHomeSet(s.config.CaldavUri, s.config.CaldavUser, s.config.CaldavPassword, homeSetUri, s.config.Calendar)
- if err != nil {
- log.Fatal("Error reading calendar URI: ", err)
+ // Find calendar URI for the calendar specified in the config
+ calendarPath, err = caldav.CalendarFromHomeSet(s.config.CaldavHost, s.config.CaldavUser, s.config.CaldavPassword, homeSetPath, s.config.Calendar)
+ if err != nil {
+ log.Fatal("Error reading calendar URI: ", err)
+ }
+ log.Printf("Calendar %d path:\t\t%s\n", s.config.Calendar, calendarPath)
}
- log.Printf("Calendar %d:\t\t%s\n", s.config.Calendar, calendarUri)
+
log.Println("----------------------------------------------------")
log.Println("Fetching availability from CalDAV server, please wait..")
- // Get availability data for the calendar specified in the config
- calData, err := caldav.GetAvailability(s.config.CaldavUri, s.config.CaldavUser, s.config.CaldavPassword, calendarUri)
+ if s.config.CaldavUri == "" {
+ // Get availability data for the calendar specified in the config
+ calData, err = caldav.GetAvailability(s.config.CaldavHost, s.config.CaldavUser, s.config.CaldavPassword, calendarPath)
+ } else {
+ // don't perform discovery, use the CALDAV_URI as calendar url
+ calData, err = caldav.GetAvailabilityByUrl(s.config.CaldavUri, s.config.CaldavUser, s.config.CaldavPassword)
+ }
+
if err != nil {
log.Fatal("Error reading calendar URI: ", err)
}
@@ -88,6 +104,6 @@ func updateDatabase(s *Server, c *cron.Cron) {
}
log.Println("Database initialized")
- log.Printf("Next db refresh (cron): %v\n", c.Entries()[0].Schedule.Next(time.Now()).Format("2006-01-02 15:04"))
+ log.Printf("Next db refresh (cron): \t%v\n", c.Entries()[0].Schedule.Next(time.Now()).Format("2006-01-02 15:04"))
log.Println("----------------------------------------------------")
}
diff --git a/server.go b/server.go
@@ -401,8 +401,16 @@ func (s *Server) UpdateAvailability(calData []caldav.CalData) error {
}
for _, e := range cal.Events() {
- start := e.Props.Get(ical.PropDateTimeStart).Value
- end := e.Props.Get(ical.PropDateTimeEnd).Value
+ startTimeProp := e.Props.Get(ical.PropDateTimeStart)
+ endTimeProp := e.Props.Get(ical.PropDateTimeEnd)
+
+ if startTimeProp == nil || endTimeProp == nil {
+ // skip events with funny format, not useful anyways
+ continue
+ }
+
+ start := startTimeProp.Value
+ end := endTimeProp.Value
status := e.Props.Get("X-MICROSOFT-CDO-INTENDEDSTATUS")
statusValue := "BUSY"