oficina / calendar Public

Clone
git clone https://amadan.net/oficina/calendar

Plain git — no account needed to clone.

Download

Download this file

1package calendar
2
3import (
4 "context"
5 "errors"
6 "fmt"
7 "time"
8
9 "gorm.io/gorm"
10
11 "amadan.net/oficina/calendar/internal/calendar/seal"
12)
13
14// The readers of booking pages. The writers are in store_booking_page.go;
15// the handle and the code are in store_booking_handle.go.
16
17// BookingPages lists a person's own pages, oldest first.
18func (s *Store) BookingPages(ctx context.Context, personID int64) ([]BookingPageView, error) {
19 var rows []BookingPage
20 if err := s.g.WithContext(ctx).Where("person_id = ?", personID).Order("id").Find(&rows).Error; err != nil {
21 return nil, fmt.Errorf("calendar: list booking pages: %w", err)
22 }
23 ids := make([]int64, len(rows))
24 for i, r := range rows {
25 ids[i] = r.ID
26 }
27 windows, err := s.windowsFor(ctx, ids)
28 if err != nil {
29 return nil, err
30 }
31 out := make([]BookingPageView, 0, len(rows))
32 for _, r := range rows {
33 v, err := s.openPage(r, windows[r.ID])
34 if err != nil {
35 return nil, err
36 }
37 out = append(out, v)
38 }
39 return out, nil
40}
41
42// BookingPage reads one of a person's own pages, scoped.
43func (s *Store) BookingPage(ctx context.Context, personID, pageID int64) (BookingPageView, error) {
44 var page BookingPage
45 err := s.g.WithContext(ctx).Where("id = ? AND person_id = ?", pageID, personID).First(&page).Error
46 if errors.Is(err, gorm.ErrRecordNotFound) {
47 return BookingPageView{}, ErrNotFound
48 }
49 if err != nil {
50 return BookingPageView{}, fmt.Errorf("calendar: read booking page: %w", err)
51 }
52 return s.viewOf(ctx, page)
53}
54
55// PublicBookingPage is the ONE unscoped reader: a stranger's URL, made
56// of a handle and a code. It requires the page to be published, and
57// every miss — no such handle, no such code, not published — is the
58// same ErrNotFound, so nothing can be enumerated by telling them apart.
59func (s *Store) PublicBookingPage(ctx context.Context, handle, code string) (BookingPageView, Person, error) {
60 host, err := s.PersonByHandle(ctx, handle)
61 if err != nil {
62 return BookingPageView{}, Person{}, err
63 }
64 var page BookingPage
65 err = s.g.WithContext(ctx).
66 Where("person_id = ? AND code = ? AND published_at IS NOT NULL", host.ID, code).
67 First(&page).Error
68 if errors.Is(err, gorm.ErrRecordNotFound) {
69 return BookingPageView{}, Person{}, ErrNotFound
70 }
71 if err != nil {
72 return BookingPageView{}, Person{}, fmt.Errorf("calendar: read public booking page: %w", err)
73 }
74 v, err := s.viewOf(ctx, page)
75 if err != nil {
76 return BookingPageView{}, Person{}, err
77 }
78 return v, host, nil
79}
80
81func (s *Store) viewOf(ctx context.Context, page BookingPage) (BookingPageView, error) {
82 windows, err := s.windowsFor(ctx, []int64{page.ID})
83 if err != nil {
84 return BookingPageView{}, err
85 }
86 return s.openPage(page, windows[page.ID])
87}
88
89// windowsFor loads the windows of several pages in one query, ordered
90// as the slot arithmetic wants them.
91func (s *Store) windowsFor(ctx context.Context, pageIDs []int64) (map[int64][]Window, error) {
92 out := map[int64][]Window{}
93 if len(pageIDs) == 0 {
94 return out, nil
95 }
96 var rows []BookingWindow
97 err := s.g.WithContext(ctx).Where("page_id IN ?", pageIDs).
98 Order("page_id, weekday, start_min").Find(&rows).Error
99 if err != nil {
100 return nil, fmt.Errorf("calendar: read booking windows: %w", err)
101 }
102 for _, r := range rows {
103 out[r.PageID] = append(out[r.PageID], Window{Weekday: time.Weekday(r.Weekday), StartMin: r.StartMin, EndMin: r.EndMin})
104 }
105 return out, nil
106}
107
108// openPage opens the sealed columns. No text in the error: it is the
109// thing we failed to decrypt.
110func (s *Store) openPage(p BookingPage, windows []Window) (BookingPageView, error) {
111 title, err := s.seal.OpenString(seal.PurposeBookingTitle, p.TitleSealed)
112 if err != nil {
113 return BookingPageView{}, fmt.Errorf("calendar: open page title (id %d): %w", p.ID, err)
114 }
115 desc, err := s.seal.OpenString(seal.PurposeBookingDescription, p.DescriptionSealed)
116 if err != nil {
117 return BookingPageView{}, fmt.Errorf("calendar: open page description (id %d): %w", p.ID, err)
118 }
119 avail, err := s.seal.OpenString(seal.PurposeBookingAvailability, p.AvailabilitySealed)
120 if err != nil {
121 return BookingPageView{}, fmt.Errorf("calendar: open page availability (id %d): %w", p.ID, err)
122 }
123 return BookingPageView{
124 BookingPage: p, Title: title, Description: desc, Availability: avail,
125 Windows: windows, Published: p.PublishedAt != nil,
126 }, nil
127}
128