commit 52627a00f3ced37817693ce60bc07aa05ba313ac
parent 7c5c5cfa62db55d2088cf89558266d382421081a
Author: Andreas Gruhler <andreas.gruhler@adfinis.com>
Date: Wed, 13 Sep 2023 01:27:48 +0200
feat: serve weeks
Diffstat:
M | go.mod | | | 3 | ++- |
M | go.sum | | | 4 | ++++ |
M | main.go | | | 8 | +++++++- |
M | server.go | | | 235 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------- |
4 files changed, 202 insertions(+), 48 deletions(-)
diff --git a/go.mod b/go.mod
@@ -13,6 +13,7 @@ require (
require (
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
+ github.com/julienschmidt/httprouter v1.3.0 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
@@ -23,7 +24,7 @@ require (
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/subosito/gotenv v1.4.2 // indirect
- github.com/teambition/rrule-go v1.7.2 // indirect
+ github.com/teambition/rrule-go v1.8.2 // indirect
golang.org/x/sys v0.8.0 // indirect
golang.org/x/text v0.9.0 // indirect
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect
diff --git a/go.sum b/go.sum
@@ -131,6 +131,8 @@ github.com/in0rdr/go-webdav v0.0.0-20230903141941-cde95745290e h1:iXhjrrNT+r6E6H
github.com/in0rdr/go-webdav v0.0.0-20230903141941-cde95745290e/go.mod h1:u8ZQQE8aW8NvpP101ttUsXcbtRKiedaHAPtsKAFpPnY=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
+github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U=
+github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
@@ -185,6 +187,8 @@ github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8
github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
github.com/teambition/rrule-go v1.7.2 h1:goEajFWYydfCgavn2m/3w5U+1b3PGqPUHx/fFSVfTy0=
github.com/teambition/rrule-go v1.7.2/go.mod h1:mBJ1Ht5uboJ6jexKdNUJg2NcwP8uUMNvStWXlJD3MvU=
+github.com/teambition/rrule-go v1.8.2 h1:lIjpjvWTj9fFUZCmuoVDrKVOtdiyzbzc93qTmRVe/J8=
+github.com/teambition/rrule-go v1.8.2/go.mod h1:Ieq5AbrKGciP1V//Wq8ktsTXwSwJHDD5mD/wLBGl3p4=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
diff --git a/main.go b/main.go
@@ -6,6 +6,7 @@ import (
"net/http"
"hivedav/config"
"hivedav/caldav"
+ "github.com/julienschmidt/httprouter"
)
func main() {
@@ -61,5 +62,10 @@ func main() {
}
log.Printf("Ready, listening on %s:%d\n", conf.ListenAddress, conf.ListenPort)
- http.ListenAndServe(fmt.Sprintf("%s:%d", conf.ListenAddress, conf.ListenPort), server)
+
+ router := httprouter.New()
+ router.GET("/", server.Index)
+ router.GET("/week/:week", server.Week)
+
+ log.Fatal(http.ListenAndServe(fmt.Sprintf("%s:%d", conf.ListenAddress, conf.ListenPort), router))
}
diff --git a/server.go b/server.go
@@ -2,8 +2,9 @@ package main
import (
"time"
+ "strconv"
+ "github.com/teambition/rrule-go"
"github.com/emersion/go-ical"
- //"github.com/arran4/golang-ical"
"hivedav/config"
"hivedav/caldav"
"fmt"
@@ -14,6 +15,7 @@ import (
"io"
"log"
"net/http"
+ "github.com/julienschmidt/httprouter"
"strings"
)
@@ -34,6 +36,13 @@ var create_availability_1 = `CREATE TABLE IF NOT EXISTS availability_1 (
start DATETIME NOT NULL,
end DATETIME NOT NULL,
tz TEXT NOT NULL);`
+var a = `SELECT * FROM availability WHERE
+ start BETWEEN '2023-01-30 15:00' AND '2023-01-30 16:00' OR
+ end BETWEEN '2023-01-30 15:00' AND '2023-01-30 16:00';`
+var available = `SELECT * FROM availability WHERE
+ (start BETWEEN ? AND ?) OR
+ (end BETWEEN ? AND ?) OR
+ (start <= ? AND end >= ?);`
func (s *Server) NewServer(c *config.Config) (*Server, error) {
s = new(Server)
@@ -59,15 +68,121 @@ func (s *Server) NewServer(c *config.Config) (*Server, error) {
return s, nil
}
-func (s *Server) ServeHTTP(w http.ResponseWriter, req *http.Request) {
+// https://xferion.com/golang-reverse-isoweek-get-the-date-of-the-first-day-of-iso-week
+func dayOfISOWeek(weekday int, week int, year int) time.Time {
+ timeNow := time.Now()
+ yearNow, weekNow:= timeNow.ISOWeek()
+
+ // iterate back to the weekday
+ for timeNow.Weekday() != time.Weekday(weekday) {
+ // years, months, days
+ timeNow = timeNow.AddDate(0, 0, -1)
+ yearNow, weekNow = timeNow.ISOWeek()
+ }
+
+ // if we iterated back into the old year, iterate forward in weekly
+ // steps (7) until we reach the weekday of the first week in yearNow
+ for yearNow < year {
+ timeNow = timeNow.AddDate(0, 0, 7)
+ yearNow, weekNow = timeNow.ISOWeek()
+ }
+
+ // iterate forward to the first day of the given ISO week
+ for weekNow < week {
+ timeNow = timeNow.AddDate(0, 0, 7)
+ yearNow, weekNow = timeNow.ISOWeek()
+ }
+
+ return timeNow
+}
+
+func (s *Server) available(start string, end string) (bool, error) {
+ var (
+ id int
+ rowStart time.Time
+ rowEnd time.Time
+ tz string
+ )
+
+ stmt, err := s.db.Prepare(available)
+
+ if err != nil {
+ log.Println("Error during db statement Prepare()")
+ return false, err
+ }
+
+ err = stmt.QueryRow(start, end, start, end, start, end).Scan(&id, &rowStart, &rowEnd, &tz)
+ if err != nil {
+ if err == sql.ErrNoRows {
+ log.Println("AVAILABLE")
+ return true, nil
+ }
+ log.Printf("Different error: %v\n", err)
+ return false, err
+ }
+ log.Println("NOT AVAILABLE")
+ return false, nil
+}
+
+func (s *Server) Week(w http.ResponseWriter, req *http.Request, ps httprouter.Params) {
userAgent := req.Header.Get("user-agent")
+
+ timeNow := time.Now()
+ weekday := timeNow.Weekday()
+ year, week := timeNow.ISOWeek()
+
+ // overwrite with requested week
+ week, err := strconv.Atoi(ps.ByName("week"))
+ if err != nil {
+ log.Printf("Cannot serve requested week '%s'")
+ w.WriteHeader(http.StatusBadRequest)
+ return
+ }
+
+ log.Printf("Current weekday is '%d'\n", weekday)
+ log.Printf("Serving week '%v' of year '%v'\n", week, year)
+
+ monday := dayOfISOWeek(1, week, year)
+ log.Printf("Monday is '%v'\n", monday)
+ sqlDateFmt := "2006-01-02 15:04"
+
if strings.Contains(userAgent, "curl") {
w.Header().Set("Content-Type", "text/plain")
io.WriteString(w, ansistyle.Bold.Open+"Welcome to the "+ansistyle.Bold.Close)
io.WriteString(w, ansistyle.BgRed.Open+"hive!"+ansistyle.BgBlue.Close+"\n")
+ io.WriteString(w, fmt.Sprintf("Serving week %d of year %d\n", week, year))
pt := prettytable.New([]string{"Time ", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday"})
- pt.AddRow("08:00 - 09:00", "", "", "busy", "", "")
+
+ monTime:= dayOfISOWeek(1, week, year)
+ timeIt := time.Date(monTime.Year(), monTime.Month(), monTime.Day(), 0, 0, 0, 0, time.UTC)
+
+ // Working hours - Eight to Five
+ for _, h := range[]int{8,9,10,11,12,13,14,15,16} {
+ var availability [5]string
+ // Working days - Monday through Friday
+ for _, d := range []int{1,2,3,4,5} {
+ start := timeIt.Add(time.Hour * time.Duration((d-1)*24 + h)).Format(sqlDateFmt)
+ end := timeIt.Add(time.Hour * time.Duration(h+1)).Format(sqlDateFmt)
+ avi, err := s.available(start, end)
+ if err != nil {
+ log.Printf("Error getting availability on day '%d' hour '%d'\n", d, h)
+ return
+ }
+ if avi {
+ availability[d-1] = ""
+ } else {
+ availability[d-1] = "X"
+ }
+ }
+ pt.AddRow(fmt.Sprintf("%02d:00 - %02d:00", h, h+1),
+ availability[0],
+ availability[1],
+ availability[2],
+ availability[3],
+ availability[4])
+ }
+
io.WriteString(w, fmt.Sprint(pt))
} else {
w.Header().Set("Content-Type", "text/html")
@@ -76,24 +191,21 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, req *http.Request) {
}
}
+func (s *Server) Index(w http.ResponseWriter, req *http.Request, _ httprouter.Params) {
+ timeNow := time.Now()
+ _, currentWeek:= timeNow.ISOWeek()
+ ps := httprouter.Params{
+ httprouter.Param{"week", strconv.Itoa(currentWeek)},
+ }
+ s.Week(w, req, ps)
+}
+
func (s *Server) updateAvailability(calData []caldav.CalData) error {
+ // TODO: Make max for recurring events without end time configurable
+ rrLimit := time.Date(2024, 12, 31, 0, 0, 0, 0, time.UTC)
+
// 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 {
@@ -101,50 +213,75 @@ func (s *Server) updateAvailability(calData []caldav.CalData) error {
}
for _, e := range cal.Events() {
+ var tz string
+ // https://github.com/emersion/go-ical/blob/master/enums.go
+ if tzProp := e.Props.Get(ical.PropTimezoneID); tzProp != nil {
+ tz = tzProp.Value
+ } else {
+ // TODO: make fallback timezone configurable
+ tz = "Europe/Zurich"
+ }
start := e.Props.Get(ical.PropDateTimeStart).Value
end := e.Props.Get(ical.PropDateTimeEnd).Value
- // try parsing all possible time formats
- startTime, err := parseTime(start)
+ // Parse all possible time formats
+ // Don't use ical DateTime(), has problems with some timezones
+ startTime, err := parseTime(start, tz)
if err != nil {
return err
}
- endTime, err := parseTime(end)
+ endTime, err := parseTime(end, tz)
if err != nil {
return err
}
+ duration := endTime.Sub(startTime)
- var tz string
- if tzProp := e.Props.Get(ical.PropTimezoneID); tzProp != nil {
- tz = tzProp.Value
- } else {
- tz = ""
+ // Parse the recurrence rules
+ // https://github.com/emersion/go-ical/blob/master/ical.go
+ // https://github.com/emersion/go-ical/blob/master/components.go
+ roption, err := e.Props.RecurrenceRule()
+ if err != nil {
+ return err
}
+ // 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
+ }
- // //localTimezone, err := time.LoadLocation("Europe/Zurich")
+ // Save recurring events
+ if roption != nil {
+ r, err := rrule.NewRRule(*roption)
+ if err != nil {
+ return err
+ }
- // // 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)
- // }
+ set := rrule.Set{}
+ set.RRule(r)
+ set.DTStart(startTime)
- // dtend, err := e.DateTimeEnd(nil)
+ // add exception dates to recurrence set exclusion list
+ exceptionDates := e.Props.Get(ical.PropExceptionDates)
+ if exceptionDates != nil {
+ exDateTime, err := parseTime(exceptionDates.Value, tz)
+ if err != nil {
+ return err
+ }
+ set.ExDate(exDateTime)
+ }
- // 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
+ // don't include the first event, handled outside the loop
+ rDates := set.Between(startTime, rrLimit, false)
+ for _, rrd := range rDates {
+ if _, err = s.db.Exec("INSERT INTO availability_1 VALUES(NULL, ?, ?, ?);", rrd, rrd.Add(duration), tz); err != nil {
+ return err
+ }
+ }
}
}
}
+
// delete current availability
if _, err := s.db.Exec("DROP TABLE IF EXISTS availability"); err != nil {
return err
@@ -157,16 +294,22 @@ func (s *Server) updateAvailability(calData []caldav.CalData) error {
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)
+func parseTime(timeStr string, tz string) (time.Time, error){
+ local, err := time.LoadLocation(tz)
+ if err != nil {
+ return time.Time{}, err
+ }
+
+ t, err := time.ParseInLocation("20060102T150405Z", timeStr, local)
if err != nil {
- t, err = time.Parse("20060102T150405", timeStr)
+ t, err = time.ParseInLocation("20060102T150405", timeStr, local)
}
if err != nil {
- t, err = time.Parse("20060102", timeStr)
+ t, err = time.ParseInLocation("20060102", timeStr, local)
}
if err != nil {
return time.Time{}, err