commit 7c5c5cfa62db55d2088cf89558266d382421081a
parent 770da5fd591500032a931250e57690a4cae81146
Author: Andreas Gruhler <andreas.gruhler@adfinis.com>
Date: Tue, 12 Sep 2023 10:29:28 +0200
feat: parse time
Diffstat:
M | .gitignore | | | 1 | + |
M | README.md | | | 51 | +++++++++++++++++++++++++++++++++++++++++++++++++-- |
M | main.go | | | 61 | +++---------------------------------------------------------- |
M | server.go | | | 129 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------- |
4 files changed, 172 insertions(+), 70 deletions(-)
diff --git a/.gitignore b/.gitignore
@@ -2,3 +2,4 @@
*.env
coverage.out
hivedav
+app.db
diff --git a/README.md b/README.md
@@ -48,16 +48,63 @@ HIVEDAV_CALDAV_PASSWORD=
There is an example config file provided in `./app.env.example`.
-## Storage
+The `HIVEDAV_CALENDAR` is a number that points to the index of the calendar
+collection in the ["calendar home
+set"](https://www.rfc-editor.org/rfc/rfc4791.html#section-6.2.1) which should
+be used to read availability data.
+
+TODO: specify calendar using the "Name" propery
+## Storage
The application stores the event start/end times in an sqlite database in the
directory where the process is running.
+The schema of the `availability` table is very simple and straight forward:
+
+```bash
+sqlite3 app.db ".schema availability"
+CREATE TABLE availability (
+ id INTEGER NOT NULL PRIMARY KEY,
+ start DATETIME NOT NULL,
+ end DATETIME NOT NULL,
+ tz TEXT NOT NULL);
+```
+
+To show the current availability time slots:
```bash
sqlite3 app.db "select * from availability;" | less
+sqlite3 app.db "select * from availability where start between '2023-01-01' and '2023-02-01';" | less
```
-TODO: Add the schema
+There exists another table `availability_1` which is used to store the new
+availability time slots during an availability update ("CalDAV request").
+During this CalDAV request, the table `availability_1` is used to temporarily
+store the new availability. After the update is done, the temporary table is
+set as the "current" availability. The application only reads availability from
+the `availability` table, not from the temporary `availability_1` table.
+
+## CalDAV Requests
+The CalDAV requests in the `caldav` module are used to query:
+* the user principal
+* the calendar home set
+* the calendar from the home set (`HIVEDAV_CALENDAR`) used as source of
+ availability data
+
+The CalDAV requests are mad according to the examples in
+https://sabre.io/dav/building-a-caldav-client.
+
+The requests are kept very simple intentionally with libraries included in Go:
+* The requests only use the `net/http` library to do the HTTP requests.
+* The response is parsed using the `encoding/xml` library
+
+No external library was used perform the requests, because these libraries
+typically assume specific behavior for the remote CalDAV server. Unfortunately,
+the servers vary in their behavior quite a lot. For instance, certain servers
+don't support the [`calendar-query
+REPORT`](https://www.rfc-editor.org/rfc/rfc4791.html#section-7.8). To make this
+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.
## Development & Contributions
* All source code is available in this repository. Contributions are always
diff --git a/main.go b/main.go
@@ -3,13 +3,9 @@ package main
import (
"fmt"
"log"
- //"time"
- "strings"
"net/http"
"hivedav/config"
"hivedav/caldav"
- "github.com/emersion/go-ical"
- //"github.com/arran4/golang-ical"
)
func main() {
@@ -59,60 +55,9 @@ func main() {
log.Fatal("Error reading calendar URI: ", err)
}
- // Walk through events and store start/end time in the database
- for _, event := range calData {
- // arran4 library has issues with parsing the VEVENT DTEND timezones below
- // cal, err := ics.ParseCalendar(strings.NewReader(event.Data))
- // if err != nil {
- // log.Fatalf("Error parsing VEVENT ics: %v", err)
- // }
-
- // for _, e := range cal.Events() {
- // dtend, err := e.GetEndAt()
- // if err != nil {
- // log.Println("Error parsing VEVENT DTEND: ", err)
- // }
- // log.Printf("%d: %v\n", dtend)
- // }
-
- // use emersion library
- dec := ical.NewDecoder(strings.NewReader(event.Data))
- cal, err := dec.Decode()
- if err != nil {
- log.Fatalf("Error decoding VEVENT ics: %v", err)
- }
-
- for _, e := range cal.Events() {
-
-
- start := e.Props.Get(ical.PropDateTimeStart).Value
- end := e.Props.Get(ical.PropDateTimeEnd).Value
- var tz string
- if tzProp := e.Props.Get(ical.PropTimezoneID); tzProp != nil {
- tz = tzProp.Value
- } else {
- tz = ""
- }
-
- // //localTimezone, err := time.LoadLocation("Europe/Zurich")
-
- // // Uses UTC when location is nil
- // // https://github.com/emersion/go-ical/blob/master/ical.go
- // // https://github.com/emersion/go-ical/issues/10
- // //dtstart, err := event.DateTimeStart(localTimezone)
- // dtstart, err := e.DateTimeStart(nil)
- // if err != nil {
- // log.Println("TZID not in tzdb form")
- // // https://github.com/emersion/go-ical/blob/master/example_test.go
- // e.Props.SetText(ical.PropTimezoneID, "Europe/Zurich")
- // dtstart, err = e.DateTimeStart(nil)
- // }
-
- // dtend, err := e.DateTimeEnd(nil)
- if _, err = server.db.Exec("INSERT INTO availability VALUES(NULL, ?, ?, ?);", start, end, tz); err != nil {
- log.Fatal("Error saving events to database", err)
- }
- }
+ err = server.updateAvailability(calData)
+ if err != nil {
+ log.Fatalf("Error fetching availability: %v", err)
}
log.Printf("Ready, listening on %s:%d\n", conf.ListenAddress, conf.ListenPort)
diff --git a/server.go b/server.go
@@ -1,7 +1,11 @@
package main
import (
+ "time"
+ "github.com/emersion/go-ical"
+ //"github.com/arran4/golang-ical"
"hivedav/config"
+ "hivedav/caldav"
"fmt"
"github.com/pquerna/termchalk/ansistyle"
"github.com/pquerna/termchalk/prettytable"
@@ -18,6 +22,19 @@ type Server struct {
db *sql.DB
}
+var drop = `DROP TABLE IF EXISTS availability;
+ DROP TABLE IF EXISTS availability_1;`
+var create_availability = `CREATE TABLE IF NOT EXISTS availability (
+ id INTEGER NOT NULL PRIMARY KEY,
+ start DATETIME NOT NULL,
+ end DATETIME NOT NULL,
+ tz TEXT NOT NULL);`
+var create_availability_1 = `CREATE TABLE IF NOT EXISTS availability_1 (
+ id INTEGER NOT NULL PRIMARY KEY,
+ start DATETIME NOT NULL,
+ end DATETIME NOT NULL,
+ tz TEXT NOT NULL);`
+
func (s *Server) NewServer(c *config.Config) (*Server, error) {
s = new(Server)
s.config = c
@@ -29,19 +46,13 @@ func (s *Server) NewServer(c *config.Config) (*Server, error) {
}
s.db = db
- drop := "DROP TABLE IF EXISTS availability;"
- create := `
- CREATE TABLE availability (
- id INTEGER NOT NULL PRIMARY KEY,
- start DATETIME NOT NULL,
- end DATETIME NOT NULL,
- tz TEXT NOT NULL
- );`
-
if _, err := db.Exec(drop); err != nil {
return nil, err
}
- if _, err := db.Exec(create); err != nil {
+ if _, err := db.Exec(create_availability); err != nil {
+ return nil, err
+ }
+ if _, err := db.Exec(create_availability_1); err != nil {
return nil, err
}
@@ -65,6 +76,104 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, req *http.Request) {
}
}
+func (s *Server) updateAvailability(calData []caldav.CalData) error {
+ // Walk through events and store start/end time in the database
+ for _, event := range calData {
+ // arran4 library has issues with parsing the VEVENT DTEND timezones below
+ // cal, err := ics.ParseCalendar(strings.NewReader(event.Data))
+ // if err != nil {
+ // log.Fatalf("Error parsing VEVENT ics: %v", err)
+ // }
+
+ // for _, e := range cal.Events() {
+ // dtend, err := e.GetEndAt()
+ // if err != nil {
+ // log.Println("Error parsing VEVENT DTEND: ", err)
+ // }
+ // log.Printf("%d: %v\n", dtend)
+ // }
+
+ // use emersion library
+ dec := ical.NewDecoder(strings.NewReader(event.Data))
+ cal, err := dec.Decode()
+ if err != nil {
+ return err
+ }
+
+ for _, e := range cal.Events() {
+
+ start := e.Props.Get(ical.PropDateTimeStart).Value
+ end := e.Props.Get(ical.PropDateTimeEnd).Value
+
+ // try parsing all possible time formats
+ startTime, err := parseTime(start)
+ if err != nil {
+ return err
+ }
+ endTime, err := parseTime(end)
+ if err != nil {
+ return err
+ }
+
+ var tz string
+ if tzProp := e.Props.Get(ical.PropTimezoneID); tzProp != nil {
+ tz = tzProp.Value
+ } else {
+ tz = ""
+ }
+
+
+ // //localTimezone, err := time.LoadLocation("Europe/Zurich")
+
+ // // Uses UTC when location is nil
+ // // https://github.com/emersion/go-ical/blob/master/ical.go
+ // // https://github.com/emersion/go-ical/issues/10
+ // //dtstart, err := event.DateTimeStart(localTimezone)
+ // dtstart, err := e.DateTimeStart(nil)
+ // if err != nil {
+ // log.Println("TZID not in tzdb form")
+ // // https://github.com/emersion/go-ical/blob/master/example_test.go
+ // e.Props.SetText(ical.PropTimezoneID, "Europe/Zurich")
+ // dtstart, err = e.DateTimeStart(nil)
+ // }
+
+ // dtend, err := e.DateTimeEnd(nil)
+
+ // set new availability in temporary table availability_1
+ if _, err = s.db.Exec("INSERT INTO availability_1 VALUES(NULL, ?, ?, ?);", startTime, endTime, tz); err != nil {
+ return err
+ }
+ }
+ }
+ // delete current availability
+ if _, err := s.db.Exec("DROP TABLE IF EXISTS availability"); err != nil {
+ return err
+ }
+ // set new availability as current availability
+ if _, err := s.db.Exec("ALTER TABLE availability_1 RENAME TO availability"); err != nil {
+ return err
+ }
+ // prepare new temporary table
+ if _, err := s.db.Exec(create_availability_1); err != nil {
+ return err
+ }
+ return nil
+}
+
+func parseTime(timeStr string) (time.Time, error){
+ t, err := time.Parse("20060102T150405Z", timeStr)
+ if err != nil {
+ t, err = time.Parse("20060102T150405", timeStr)
+ }
+ if err != nil {
+ t, err = time.Parse("20060102", timeStr)
+ }
+ if err != nil {
+ return time.Time{}, err
+ }
+ return t, nil
+}
+
func (s *Server) showCubicles() {
}