Author: Adam <git@apiote.xyz>
add holiday calendar
holidays.dirty.example | 7 + router.go | 182 +++++++++++++++++++++++++++++++++++++++++++ templates/calendar.ics | 19 ++++
diff --git a/holidays.dirty.example b/holidays.dirty.example new file mode 100644 index 0000000000000000000000000000000000000000..489152dd2a6c2351c3f1078a5f6f88305fd737eb --- /dev/null +++ b/holidays.dirty.example @@ -0,0 +1,7 @@ +( + ( + ('name' 'New Year’s Day') + ('month' 1) + ('day' 1) + ) +) diff --git a/router.go b/router.go index e612c2e049791e79ebdbdaf779faafa6e5e5af7a..8598d7188df2df0ef265a7705b67572e5dc2ce73 100644 --- a/router.go +++ b/router.go @@ -1,6 +1,7 @@ package main import ( + "bytes" "fmt" "html/template" "io" @@ -10,8 +11,9 @@ "math/rand" "mime" "net/http" "os" + "strconv" "strings" - "bytes" + "time" "apiote.xyz/p/go-dirty" "github.com/bytesparadise/libasciidoc" @@ -38,6 +40,44 @@ Versions map[string]Version Tag string Module string Content template.HTML +} + +type Holiday struct { + Name string + Month int + Day int + Rrule string + Year int +} + +func (h Holiday) Format() string { + var result string + if h.Year > 0 { + result = strconv.FormatInt(int64(h.Year), 10) + } else { + result = "2001" + } + if h.Month < 10 { + result += "0" + } + result += strconv.FormatInt(int64(h.Month), 10) + if h.Day < 10 { + result += "0" + } + result += strconv.FormatInt(int64(h.Day), 10) + + return result +} + +func (h Holiday) UID() string { + uid := strings.ReplaceAll(h.Name, " ", "") + uid = strings.ReplaceAll(uid, "’", "") + uid = strings.ReplaceAll(uid, ".", "") + uid = strings.ToLower(uid) + if h.Year != 0 { + uid = uid + "_" + strconv.FormatInt(int64(h.Year), 10) + } + return uid + "@apiote.xyz" } func static(w http.ResponseWriter, r *http.Request) { @@ -233,6 +273,145 @@ } } } +func easter(y int) []Holiday { + a := y % 19 + b := y % 4 + c := y % 7 + p := y / 100 + q := (13 + (8 * p)) / 25 + m := (15 - q + p - (p / 4)) % 30 + n := (4 + p - (p / 4)) % 7 + d := (19*a + m) % 30 + e := (n + (2 * b) + (4 * c) + (6 * d)) % 7 + + var easterDate time.Time + if d == 29 && e == 6 { + easterDate = time.Date(y, 4, 19, 0, 0, 0, 0, time.Local) + } else if d == 28 && e == 6 { + easterDate = time.Date(y, 4, 18, 0, 0, 0, 0, time.Local) + } else { + easterDate = time.Date(y, 3, 22, 0, 0, 0, 0, time.Local).AddDate(0, 0, d+e) + } + + holidays := []Holiday{} + holidays = append(holidays, Holiday{ + Name: "Easter", + Month: int(easterDate.Month()), + Day: easterDate.Day(), + Year: y, + }) + h := easterDate.AddDate(0, 0, -52) + holidays = append(holidays, Holiday{ + Name: "Fat Thursday", + Month: int(h.Month()), + Day: h.Day(), + Year: y, + }) + h = easterDate.AddDate(0, 0, -47) + holidays = append(holidays, Holiday{ + Name: "Shrove Tuesday", + Month: int(h.Month()), + Day: h.Day(), + Year: y, + }) + h = easterDate.AddDate(0, 0, -46) + holidays = append(holidays, Holiday{ + Name: "Ash Wednesday", + Month: int(h.Month()), + Day: h.Day(), + Year: y, + }) + h = easterDate.AddDate(0, 0, -7) + holidays = append(holidays, Holiday{ + Name: "Palm Sunday", + Month: int(h.Month()), + Day: h.Day(), + Year: y, + }) + h = easterDate.AddDate(0, 0, -3) + holidays = append(holidays, Holiday{ + Name: "Maundy Thursday", + Month: int(h.Month()), + Day: h.Day(), + Year: y, + }) + h = easterDate.AddDate(0, 0, -2) + holidays = append(holidays, Holiday{ + Name: "Good Friday", + Month: int(h.Month()), + Day: h.Day(), + Year: y, + }) + h = easterDate.AddDate(0, 0, -1) + holidays = append(holidays, Holiday{ + Name: "Easter Eve", + Month: int(h.Month()), + Day: h.Day(), + Year: y, + }) + h = easterDate.AddDate(0, 0, 1) + holidays = append(holidays, Holiday{ + Name: "Easter Monday", + Month: int(h.Month()), + Day: h.Day(), + Year: y, + }) + h = easterDate.AddDate(0, 0, 49) + holidays = append(holidays, Holiday{ + Name: "Pentecost", + Month: int(h.Month()), + Day: h.Day(), + Year: y, + }) + h = easterDate.AddDate(0, 0, 60) + holidays = append(holidays, Holiday{ + Name: "Corpus Christi", + Month: int(h.Month()), + Day: h.Day(), + Year: y, + }) + + return holidays +} + +func calendar(w http.ResponseWriter, r *http.Request) { + file, err := os.Open("holidays.dirty") + if err != nil { + log.Println(err) + fmt.Fprint(w, "Error 500") + return + } + holidays := []Holiday{} + err = dirty.LoadStruct(file, &holidays) + if err != nil { + log.Println(err) + log.Println(holidays) + fmt.Fprint(w, "Error 500") + return + } + t, err := template.ParseFiles("templates/calendar.ics") + if err != nil { + log.Println(err) + if os.IsNotExist(err) { + renderStatus(w, 501) + return + } + fmt.Fprint(w, "Error 500") + return + } + thisYear := time.Now().Year() + for i := 0; i < 10; i++ { + holidays = append(holidays, easter(thisYear+i)...) + } + w.Header().Set("Content-Type", "text/calendar") + err = t.Execute(w, holidays) + if err != nil { + log.Println(err) + fmt.Fprint(w, "Error 500") + return + } +} + func shortRedirect(w http.ResponseWriter, r *http.Request) { path := strings.Split(r.URL.Path[1:], "/") if len(path) == 2 && path[1] == "" { @@ -386,6 +565,7 @@ http.HandleFunc("/programs/", programs) http.HandleFunc("/donate", donate) http.HandleFunc("/donate/", donate) // xxx historical reasons for trailing slash http.HandleFunc("/pgp/", pgp) + http.HandleFunc("/calendar", calendar) http.HandleFunc("/s/", shortRedirect) http.HandleFunc("/short/", shortShow) diff --git a/templates/calendar.ics b/templates/calendar.ics new file mode 100644 index 0000000000000000000000000000000000000000..c0e172217387e2f6e56ea3d5efdf1af3a6b54239 --- /dev/null +++ b/templates/calendar.ics @@ -0,0 +1,19 @@ +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:apiote.xyz +COLOR:royalBlue +{{ range . }} +BEGIN:VEVENT +UID:{{ .UID }} +DTSTAMP:20221114T172530Z +DTSTART:{{ .Format }}T000000 +DTEND:{{ .Format }}T240000 +SUMMARY:{{ .Name }} +{{- if .Rrule }} +RRULE:{{ .Rrule }} +{{- else if eq .Year 0 }} +RRULE:FREQ=YEARLY +{{- end }} +END:VEVENT +{{ end }} +END:VCALENDAR