drop calendar module
7 files changed, 0 insertions(+), 519 deletions(-)
changed files
D domain/calendar/calendar.go
@@ -1,309 +0,0 @@ -package calendar - -import ( - "context" - "errors" - "fmt" - "io" - "io/fs" - "net/http" - "os" - "slices" - "time" - - "alin.ovh/x/log" - "github.com/Southclaws/fault" - "github.com/Southclaws/fault/fmsg" - ical "vimagination.zapto.org/ics" - - "alin.ovh/homestead/shared/cache" - "alin.ovh/homestead/shared/config" -) - -type Options struct { - URL config.URL - Timezone config.Timezone - Cache bool -} - -type Calendar struct { - opts *Options - log *log.Logger - client *http.Client - - *ical.Calendar -} - -type Busy struct { - ical.FreeBusy - StartTime Date - EndTime Date - Type ical.ParamFreeBusyType -} - -type Date struct { - time.Time -} - -type CalendarDate struct { - Date - BusyPeriods []*Busy -} - -const ( - Filename = "calendar.ics" - Refresh = 30 * time.Minute -) - -func New(opts *Options, logger *log.Logger) *Calendar { - if opts.URL.Scheme == "webcal" { - opts.URL.Scheme = "https" - } - - return &Calendar{ - opts: opts, - log: logger, - client: &http.Client{ - Timeout: time.Second * 10, - }, - } -} - -func (c *Calendar) ValidCache() (bool, error) { - if !c.opts.Cache { - return false, nil - } - - stat, err := cache.Root.Stat(Filename) - if err != nil && !errors.Is(err, fs.ErrNotExist) { - return false, fault.Wrap(err, fmsg.With("could not stat calendar file")) - } - - return stat != nil && time.Since(stat.ModTime()) < Refresh && stat.Size() > 0, nil -} - -func (c *Calendar) FetchIfNeeded(ctx context.Context) error { - var err error - - if valid, err := c.ValidCache(); err != nil { - return err - } else if !valid { - err := c.fetch(ctx) - if err != nil { - return err - } - } - - f, err := c.open() - if err != nil { - return err - } - defer f.Close() - - c.Calendar, err = ical.Decode(f) - if err != nil { - c.log.Warn("error parsing calendar", "error", err) - - return fault.Wrap(err, fmsg.With("could not parse calendar")) - } - - return err -} - -func (c *Calendar) EventsBetween(from time.Time, to time.Time) ([]*Busy, error) { - if c.Calendar == nil { - return nil, fault.New("calendar not initialised") - } - - bs := make([]*Busy, 0, len(c.FreeBusy)) - - for _, fb := range c.FreeBusy { - for _, b := range fb.FreeBusy { - st := b.Start - et := b.End - - if st.Before(from) && et.Before(from) || - st.After(to) && et.After(to) { - continue - } - - bs = append(bs, &Busy{ - FreeBusy: fb, - Type: *b.FreeBusyType, - StartTime: Date{Time: st.In(c.opts.Timezone.Location)}, - EndTime: Date{Time: et.In(c.opts.Timezone.Location)}, - }) - } - } - - slices.SortFunc(bs, func(a, b *Busy) int { - return a.StartTime.Compare(b.StartTime.Time) - }) - - return bs, nil -} - -func (c *Calendar) Weeks(count int) []Date { - now := time.Now() - weekday := (7 + int(now.Weekday()-time.Monday)) % 7 - start := time.Date( - now.Year(), - now.Month(), - now.Day()-(weekday), - 0, - 0, - 0, - 0, - c.opts.Timezone.Location, - ) - days := count*7 + weekday + 1 - dates := make([]Date, 0, days) - - for i := start; i.Before(start.AddDate(0, 0, days)); i = i.AddDate(0, 0, 1) { - dates = append(dates, Date{Time: i}) - } - - return dates -} - -func (c *Calendar) Availability(weeks int) ([]*CalendarDate, error) { - if !c.opts.Cache { - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - err := c.FetchIfNeeded(ctx) - cancel() - if err != nil { - return nil, err - } - } - - cds := make([]*CalendarDate, 0, weeks*7) - dates := c.Weeks(weeks) - - for _, date := range dates { - c.log.Debug("processing date", "date", date.DateOnly()) - - cd := &CalendarDate{Date: date} - - evs, err := c.EventsBetween(date.Add(1*time.Second), date.EndOfDay().Time) - if err != nil { - return nil, fault.Wrap(err, fmsg.With("could not get events")) - } - - for _, ev := range evs { - c.log.Debug("processing event", "id", ev.UID) - - cd.BusyPeriods = append(cd.BusyPeriods, ev) - } - - cds = append(cds, cd) - } - - return cds, nil -} - -func (c *Calendar) open() (*os.File, error) { - f, err := cache.Root.Open(Filename) - if err != nil { - return nil, fault.Wrap(err, fmsg.With("could not open calendar file")) - } - - return f, nil -} - -func (c *Calendar) fetch(ctx context.Context) error { - c.log.Debug("fetching calendar", "url", c.opts.URL.String()) - - f, err := cache.Root.OpenFile(Filename, os.O_RDWR|os.O_CREATE, 0o600) - if err != nil { - return fault.Wrap(err, fmsg.With("could not create temp file")) - } - defer f.Close() - - req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.opts.URL.String(), nil) - if err != nil { - return fault.Wrap(err, fmsg.With("could not create request")) - } - - res, err := c.client.Do(req) - if err != nil { - return fault.Wrap(err, fmsg.With("could not fetch calendar")) - } - defer res.Body.Close() - - if res.StatusCode != http.StatusOK { - return fault.New(fmt.Sprintf("unexpected status code %d", res.StatusCode)) - } - - if _, err := io.Copy(f, res.Body); err != nil { - return fault.Wrap(err, fmsg.With("could not write calendar to file")) - } - - err = f.Sync() - if err != nil { - return fault.Wrap(err, fmsg.With("could not sync file")) - } - - return nil -} - -func (d Date) Between(lower, upper Date) bool { - return d.After(lower) && d.Before(upper) -} - -func (d Date) Key() int { - return d.Year()*10000 + - int(d.Month())*100 + - d.Day() -} - -func (d Date) BeginningOfDay() Date { - return Date{ - Time: time.Date( - d.Year(), - d.Month(), - d.Day(), - 0, - 0, - 0, - 0, - d.Location(), - ), - } -} - -func (d Date) EndOfDay() Date { - return Date{ - Time: time.Date( - d.Year(), - d.Month(), - d.Day(), - 23, - 59, - 59, - 999999999, - d.Location(), - ), - } -} - -func (d Date) DateOnly() string { - return d.Format(time.DateOnly) -} - -func (d Date) NextDay() Date { - return Date{ - Time: d.AddDate(0, 0, 1), - } -} - -func (d Date) IsToday() bool { - return d.Key() == Date{time.Now()}.Key() -} - -func (d Date) Before(other Date) bool { - return d.Time.Before(other.Time) -} - -func (d Date) After(other Date) bool { - return d.Time.After(other.Time) -}
D domain/calendar/templates/calendar.go
@@ -1,95 +0,0 @@ -package templates - -import ( - "fmt" - "time" - - g "alin.ovh/gomponents" - c "alin.ovh/gomponents/components" - . "alin.ovh/gomponents/html" - "vimagination.zapto.org/ics" - - "alin.ovh/homestead/domain/calendar" - "alin.ovh/homestead/domain/web/templates" -) - -func CalendarPage(site templates.SiteSettings, cal *calendar.Calendar) g.Node { - return templates.Layout(site, templates.PageSettings{ - Title: "Calendar", - TitleAttrs: templates.Attrs{}, - BodyAttrs: templates.Attrs{}, - HeadExtra: g.Group{ - Link(Rel("stylesheet"), Href("/calendar.css")), - }, - }, Calendar(site, cal)) -} - -func Calendar(site templates.SiteSettings, cal *calendar.Calendar) g.Node { - past := true - dates, err := cal.Availability(2) - if err != nil { - panic(err) - } - - return Div(Class("calendar"), - H2(g.Text("Calendar")), - P(g.Text("Here you can roughly see when I'm busy in the next two weeks.")), - P(Class("only-small-portrait"), g.Text("Rotate to see calendar view and previous days.")), - Section(Class("calendar-grid"), - g.Map(dates, func(date *calendar.CalendarDate) g.Node { - if past && date.IsToday() { - past = false - } - - return Section(c.Classes{ - "day": true, - "past": past, - "today": date.IsToday(), - }, - H3( - Time( - DateTime(date.Format(time.DateOnly)), - g.Text(date.Format("Mon _2"))), - ), - g.Map(date.BusyPeriods, func(e *calendar.Busy) g.Node { - return Div( - c.Classes{ - "event": true, - "tentative": e.Type == ics.FreeBusyTypeBusyTentative, - }, - TitleAttr( - fmt.Sprintf( - "%s–%s", - e.StartTime.Format(time.RFC1123), - e.EndTime.Format(time.RFC1123), - ), - ), - g.If(e.StartTime.Before(date.BeginningOfDay()), - CTime(date.BeginningOfDay()), - CTime(e.StartTime), - ), - g.Text("–"), - g.If(e.EndTime.After(date.EndOfDay()), - CTime(date.EndOfDay()), - CTime(e.EndTime), - ), - g.If(e.Type == ics.FreeBusyTypeBusyTentative, - g.Text("?"), - ), - ) - }), - ) - }), - ), - Footer( - P(g.Textf("Timezone is %s", site.Timezone.String())), - ), - ) -} - -func CTime(date calendar.Date) g.Node { - return Time( - DateTime(date.Format(time.RFC3339)), - g.Text(date.Format("15:04")), - ) -}
M domain/web/mux.go → domain/web/mux.go
@@ -10,7 +10,6 @@ "github.com/kevinpollet/nego" "alin.ovh/homestead/domain/analytics" - calendar "alin.ovh/homestead/domain/calendar/templates" "alin.ovh/homestead/domain/web/server" "alin.ovh/homestead/domain/web/templates" ihttp "alin.ovh/homestead/shared/http"@@ -86,16 +85,6 @@ w.Header().Add(k, v) } } http.ServeContent(w, r, file.Path, file.LastModified, file.Encodings[enc]) - - return nil -} - -func (website *Website) Calendar(w http.ResponseWriter, r *http.Request) error { - analytics.WithTitle(r, "Calendar") - err := calendar.CalendarPage(*website.siteSettings, website.calendar).Render(w) - if err != nil { - return ErrRenderFailure.WithCause(err) - } return nil }
D domain/web/templates/calendar.css
@@ -1,71 +0,0 @@ -.calendar-grid { - display: grid; - grid-template-columns: repeat(7, 1fr); - grid-template-rows: repeat(2, 1fr); - gap: 0.5ch; -} - -.day { - min-height: 6rem; -} - -.past { - opacity: 0.5; -} - -.day h3 { - margin-block: 0 0.5ch; - font-size: 1.1rem; - font-weight: normal; -} - -.day.today h3 { - font-weight: bold; -} - -.event { - font-size: 0.9rem; -} - -.tentative { - opacity: 0.75; -} - -.today { - background-color: var(--today-background-color); -} - -.calendar footer { - font-size: small; - color: var(--muted-color); - padding: 1ch 0; -} - -.calendar footer p { - margin: 0; -} - -.only-small-portrait { - display: none; -} - -@media only screen and (max-width: 768px) { - .calendar-grid { - display: flex; - flex-direction: column; - } - - .past { - display: none; - } - - .day { - min-height: revert; - } -} - -@media only screen and (max-width: 768px) and (orientation: portrait) and (min-height: 768px) { - .only-small-portrait { - display: revert; - } -}
M domain/web/website.go → domain/web/website.go
@@ -1,11 +1,9 @@ package website import ( - "context" "net/http" "os" "sync" - "time" "alin.ovh/x/log" "github.com/Southclaws/fault"@@ -15,7 +13,6 @@ "alin.ovh/homestead/domain/analytics" "alin.ovh/homestead/domain/analytics/goatcounter" "alin.ovh/homestead/domain/analytics/nullcounter" - "alin.ovh/homestead/domain/calendar" "alin.ovh/homestead/domain/content/fetcher" "alin.ovh/homestead/domain/content/publisher" "alin.ovh/homestead/domain/identity"@@ -36,7 +33,6 @@ Redirect bool `conf:"default:true"` Development bool `conf:"default:false,flag:dev"` FetchURL config.URL `conf:"default:https://ci.alin.ovh/archive/website/"` BaseURL config.URL - CalendarURL config.URL Timezone config.Timezone `conf:"default:Europe/Berlin"` Goatcounter goatcounter.Options@@ -53,7 +49,6 @@ siteSettings *templates.SiteSettings counter analytics.Counter log *log.Logger reader storage.Reader - calendar *calendar.Calendar identity *identity.Service publisher *publisher.Service *server.App@@ -94,12 +89,6 @@ if err != nil { return nil, fault.Wrap(err, fmsg.With("could not create update listener")) } - website.calendar = calendar.New(&calendar.Options{ - URL: opts.CalendarURL, - Timezone: opts.Timezone, - Cache: !opts.Development, - }, log.Named("calendar")) - website.identity = identity.New(opts.OIDC, log.Named("identity")) website.publisher, err = publisher.New(&publisher.Options{ Development: opts.Development,@@ -140,11 +129,6 @@ go func() { updated := sync.OnceFunc(func() { firstUpdate <- true close(firstUpdate) - - website.FetchCalendar() - for range time.Tick(15 * time.Minute) { - website.FetchCalendar() - } }) for root := range roots { log.Debug("getting config from source", "source", root)@@ -182,7 +166,6 @@ mux := ihttp.NewServeMux(log.Named("http")) mux.HandleError(website.ErrorHandler) mux.Handle("/", website) - mux.HandleFunc("/calendar", website.Calendar) website.identity.RegisterHandlers(mux) if website.publisher != nil { website.publisher.RegisterHandlers(mux)@@ -196,7 +179,6 @@ return nil } mux.HandleFunc("/style.css", staticHandler) - mux.HandleFunc("/calendar.css", staticHandler) } website.Handler = analytics.CounterMiddleware(website.counter, mux)@@ -214,11 +196,3 @@ } return nil } - -func (w *Website) FetchCalendar() { - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) - if err := w.calendar.FetchIfNeeded(ctx); err != nil { - w.log.Error("could not fetch calendar", "error", err) - } - cancel() -}
M go.mod → go.mod
@@ -27,7 +27,6 @@ github.com/yuin/goldmark v1.7.13 go.hacdias.com/indielib v0.4.3 go.uber.org/zap v1.27.1 golang.org/x/oauth2 v0.34.0 - vimagination.zapto.org/ics v1.0.1 ) require (@@ -77,7 +76,6 @@ golang.org/x/sys v0.43.0 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect moul.io/zapfilter v1.7.0 // indirect - vimagination.zapto.org/parser v1.2.3 // indirect willnorris.com/go/microformats v1.2.1-0.20240301064101-b5d1b9d2120e // indirect willnorris.com/go/webmention v0.0.0-20250531043116-33a44c5fb605 // indirect )
M go.sum → go.sum
@@ -277,11 +277,6 @@ gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= moul.io/zapfilter v1.7.0 h1:7aFrG4N72bDH9a2BtYUuUaDS981Dxu3qybWfeqaeBDU= moul.io/zapfilter v1.7.0/go.mod h1:M+N2s+qZiA+bzRoyKMVRxyuERijS2ovi2pnMyiOGMvc= -vimagination.zapto.org/ics v1.0.1 h1:7qUq+bWauYIcuFWHQCtVbI96z4LHixpz7QZu79VGy4E= -vimagination.zapto.org/ics v1.0.1/go.mod h1:O5KGgzSkvv34oZl1035lx7rppdJviJHnfK+tlToJK5E= -vimagination.zapto.org/parser v1.0.3/go.mod h1:/H483adsvbp1SLfB0xfFBVfh+mze11+FHwCijc0rpCc= -vimagination.zapto.org/parser v1.2.3 h1:OxtKZw0slZkt8c9ixfUAAWd6t30UwZerxPJJSyG34z0= -vimagination.zapto.org/parser v1.2.3/go.mod h1:MxM1MZY6A9AeEx6nahIQ6MJaGZqX80n6Vm3zGVqV9FU= willnorris.com/go/microformats v1.2.1-0.20240301064101-b5d1b9d2120e h1:TRIOwo0NxN4KVSgYlYmiQktd9I96YgZ3942/JVzhwTM= willnorris.com/go/microformats v1.2.1-0.20240301064101-b5d1b9d2120e/go.mod h1:zzo0hFA/E/nl1ZAjXiXA7KCKwCTdgBU+7HXltGgHeGA= willnorris.com/go/webmention v0.0.0-20250531043116-33a44c5fb605 h1:emBiWPMoSWXbAwOBbDd/2RSKdIyAw38S4UlFB91OzoY=