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 "fmt"
6 "gorm.io/gorm"
7 "time"
8
9 "amadan.net/oficina/calendar/internal/calendar/seal"
10)
11
12// SetAttendees replaces an event's attendee list.
13//
14// The caller must ALREADY have read the event through a grant join —
15// this takes an id, not a person, and does no scoping of its own. Every
16// current caller comes straight off Store.Event or Store.CreateEvent,
17// both of which 404 a row that is not yours.
18func (s *Store) SetAttendees(ctx context.Context, eventID int64, addresses []string) error {
19 return s.setAttendeesOn(s.g.WithContext(ctx), eventID, addresses)
20}
21
22// setAttendeesOn is SetAttendees on a caller's transaction, so a
23// booking's confirmation can record the booker in the same commit.
24func (s *Store) setAttendeesOn(tx *gorm.DB, eventID int64, addresses []string) error {
25 if err := tx.
26 Where("event_id = ?", eventID).Delete(&Attendee{}).Error; err != nil {
27 return fmt.Errorf("calendar: clear attendees: %w", err)
28 }
29 seen := map[string]bool{}
30 now := time.Now().UTC()
31 for _, addr := range addresses {
32 addr = normalizeAddress(addr)
33 if addr == "" {
34 continue
35 }
36 fp, err := s.seal.Fingerprint(seal.PurposeAttendeeLookup, addr)
37 if err != nil {
38 return fmt.Errorf("calendar: fingerprint attendee: %w", err)
39 }
40 if seen[fp] {
41 continue
42 }
43 seen[fp] = true
44 sealed, err := s.seal.SealString(seal.PurposeAttendeeAddress, addr)
45 if err != nil {
46 return fmt.Errorf("calendar: seal attendee: %w", err)
47 }
48 row := Attendee{
49 EventID: eventID,
50 AddressSealed: sealed,
51 AddressFP: fp,
52 CreatedAt: now,
53 }
54 if err := tx.Create(&row).Error; err != nil {
55 return fmt.Errorf("calendar: add attendee: %w", err)
56 }
57 }
58 return nil
59}
60
61// AttendeesFor opens an event's attendee addresses, oldest first.
62//
63// Scoping is the caller's, exactly as in SetAttendees.
64func (s *Store) AttendeesFor(ctx context.Context, eventID int64) ([]string, error) {
65 var rows []Attendee
66 if err := s.g.WithContext(ctx).
67 Where("event_id = ?", eventID).
68 Order("id ASC").Find(&rows).Error; err != nil {
69 return nil, fmt.Errorf("calendar: list attendees: %w", err)
70 }
71 out := make([]string, 0, len(rows))
72 for _, row := range rows {
73 addr, err := s.seal.OpenString(seal.PurposeAttendeeAddress, row.AddressSealed)
74 if err != nil {
75 return nil, fmt.Errorf("calendar: open attendee: %w", err)
76 }
77 if addr != "" {
78 out = append(out, addr)
79 }
80 }
81 return out, nil
82}
83
84// AttachAttendees fills in EventView.Attendees for a slice about to be
85// exported.
86//
87// One query for the whole slice rather than one per event: an export of
88// a year's standups is a thousand EventViews, and a read per event
89// turns a feed poll into a thousand round trips on a platform billed by
90// residency.
91//
92// Only the export path calls this. A month view prints no attendee, so
93// paying for one there would be a cost with no reader.
94func (s *Store) AttachAttendees(ctx context.Context, events []EventView) error {
95 if len(events) == 0 {
96 return nil
97 }
98 // An expanded occurrence and its master share one row of attendees,
99 // so the master's id is the one to ask for.
100 owner := func(e EventView) int64 {
101 if e.SeriesID != nil && *e.SeriesID != 0 {
102 return *e.SeriesID
103 }
104 return e.ID
105 }
106
107 ids := make([]int64, 0, len(events))
108 seen := map[int64]bool{}
109 for _, e := range events {
110 id := owner(e)
111 if !seen[id] {
112 seen[id] = true
113 ids = append(ids, id)
114 }
115 }
116
117 var rows []Attendee
118 if err := s.g.WithContext(ctx).
119 Where("event_id IN ?", ids).Order("id ASC").Find(&rows).Error; err != nil {
120 return fmt.Errorf("calendar: list attendees for export: %w", err)
121 }
122 byEvent := map[int64][]string{}
123 for _, row := range rows {
124 addr, err := s.seal.OpenString(seal.PurposeAttendeeAddress, row.AddressSealed)
125 if err != nil {
126 return fmt.Errorf("calendar: open attendee for export: %w", err)
127 }
128 if addr != "" {
129 byEvent[row.EventID] = append(byEvent[row.EventID], addr)
130 }
131 }
132 for i := range events {
133 events[i].Attendees = byEvent[owner(events[i])]
134 }
135 return nil
136}
137