package main
import (
"bytes"
"log"
"net/http"
"net/url"
"strings"
"time"
"github.com/emersion/go-ical"
"golang.org/x/crypto/argon2"
)
func main() {
cfg := loadConfig()
printConfig(&cfg)
amsTz, err := time.LoadLocation("Europe/Amsterdam")
if err != nil {
log.Fatalln("Failed to load time zone", err)
}
handler := handler{
calURL: url.URL(cfg.CalendarURL.v),
ignore: cfg.Ignore,
tokens: cfg.UserTokens,
amsTz: amsTz,
userAgent: cfg.UserAgent,
}
mux := http.ServeMux{}
mux.HandleFunc("/", handler.handle)
server := http.Server{
Addr: ":" + cfg.Port,
Handler: &mux,
ReadTimeout: 5 * time.Second,
WriteTimeout: 30 * time.Second,
IdleTimeout: 30 * time.Second,
ReadHeaderTimeout: 2 * time.Second,
}
log.Println("Listening on port", cfg.Port)
log.Fatal(server.ListenAndServe())
}
type handler struct {
ignore ignoreRules
tokens map[string]userToken
calURL url.URL
amsTz *time.Location
userAgent string
}
func (h handler) makeTokenURL(token string) string {
newURL := h.calURL
query := newURL.Query()
query.Set("token", token)
newURL.RawQuery = query.Encode()
return newURL.String()
}
func (h handler) handle(w http.ResponseWriter, r *http.Request) {
brightspaceToken := r.URL.Query().Get("brightspace_token")
proxyUserID := r.URL.Query().Get("proxy_user_id")
proxyToken := r.URL.Query().Get("proxy_token")
if !userOK(h.tokens, proxyUserID, []byte(proxyToken)) {
http.Error(w, "Bad proxy_user_id or proxy_token",
http.StatusUnauthorized)
return
}
req, err := http.NewRequestWithContext(r.Context(), http.MethodGet,
h.makeTokenURL(brightspaceToken), nil)
if err != nil {
http.Error(w, "Error constructing HTTP request for Brightspace",
http.StatusInternalServerError)
return
}
if h.userAgent != "" {
req.Header.Add("User-Agent", h.userAgent)
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
http.Error(w, "Error sending request to Brightspace",
http.StatusInternalServerError)
return
}
cal, err := ical.NewDecoder(resp.Body).Decode()
if err != nil {
http.Error(w, "Could not decode iCal", http.StatusInternalServerError)
return
}
filter(h.ignore, cal.Component)
skewMidnightDeadlines(h.amsTz, cal.Component)
if err = ical.NewEncoder(w).Encode(cal); err != nil {
log.Println("Error writing calendar:", err)
}
}
func filter(ignore ignoreRules, c *ical.Component) {
j := 0
for _, child := range c.Children {
keep := true
if child.Name == ical.CompEvent {
if location := child.Props.Get(ical.PropLocation); location != nil {
for _, locationRule := range ignore.LocationRegexes {
if locationRule.MatchString(location.Value) {
keep = false
}
}
}
if summary := child.Props.Get(ical.PropSummary); summary != nil {
for _, summaryRule := range ignore.SummaryRegexes {
if summaryRule.MatchString(summary.Value) {
keep = false
}
}
}
}
if keep {
c.Children[j] = child
j++
}
}
c.Children = c.Children[:j]
}
// We often see events which are scheduled for exactly 'end of day', i.e.
// 23:59:59, with no difference between the start/end time. Unfortunately,
// these are not shown properly in two open-source Android calendar apps, so we
// monkey-hack the event to start at 23:30 and add a note to the description :)
func skewMidnightDeadlines(ams *time.Location, c *ical.Component) {
for _, child := range c.Children {
if child.Name == ical.CompEvent {
if summary := child.Props.Get(ical.PropSummary); summary != nil &&
strings.HasSuffix(summary.Value, " - Due") {
// Modify the start time of the event
startTime, stErr := child.Props.DateTime(ical.PropDateTimeStart, ams)
endTime, etErr := child.Props.DateTime(ical.PropDateTimeEnd, ams)
if stErr != nil || etErr != nil || startTime.IsZero() ||
endTime.IsZero() || !startTime.Equal(endTime) ||
startTime.In(ams).Hour() != 23 ||
startTime.Minute() != 59 || startTime.Second() != 59 {
continue
}
newStartTime := time.Date(startTime.Year(), startTime.Month(),
startTime.Day(), 23, 30, 0, 0, ams)
child.Props.SetDateTime(ical.PropDateTimeStart, newStartTime)
// Update the event description to notify about the change
descProp := child.Props.Get(ical.PropDescription)
if descProp == nil {
descProp = ical.NewProp(ical.PropDescription)
}
curText, err := descProp.Text()
if err != nil {
// forget about editing the text, at least we 'fixed' the
// event time :)
continue
}
curText = "+++ ICALPROXY: DEADLINE MOVED FROM 23:59:59 TO " +
"23:30:00 (Europe/Amsterdam) +++\n\n" + curText
descProp.SetText(curText)
child.Props.Set(descProp)
}
}
}
}
func userOK(tokens map[string]userToken, id string, token []byte) bool {
info, ok := tokens[id]
if !ok {
return false
}
return bytes.Compare(hash(token, info.Salt), info.Hash) == 0
}
func hash(token []byte, salt []byte) []byte {
return argon2.IDKey(token, salt, 1, 64*1024, 4, 32)
}