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 "time"
4
5// Models is every model the schema generator manages. Keep it in step
6// with the structs below and in models_event.go: `rastrillo migration
7// generate` reads it to work out what the database should look like,
8// and `rastrillo migration check` fails CI when it and the migrations
9// disagree.
10var Models = []any{
11 &Person{},
12 &Calendar{},
13 &Grant{},
14 &Event{},
15 &Attendee{},
16 &Reminder{},
17 &ReminderQueue{},
18 &Feed{},
19 &Meta{},
20 &BookingPage{},
21 &BookingWindow{},
22 &Booking{},
23 &BookingMail{},
24}
25
26// Person is an address that has signed in here, and the row everything
27// owned is scoped to.
28//
29// The indirection is not ceremony. Sign-in is a magic link, so the
30// session Subject is a verified email address, not a numeric id — which
31// is why sessions.UserID is unusable under this plugin and returns
32// (0, false). Owning rows by an address column instead would scatter a
33// piece of personal data across every table and make a change of
34// address a migration; owning them by this row's id keeps the address
35// in one place.
36//
37// Named Person rather than the brainstorm's Member, matching docs.
38// Sheets calls the same row Member. Nothing turns on it, and the table
39// shipped before the difference was noticed, so it stays: a rename is
40// indistinguishable from a drop plus an add, and this one buys nothing.
41type Person struct {
42 ID int64 `gorm:"primaryKey"`
43
44 // Address is the verified sign-in address, normalised (trimmed,
45 // lowercased) by normalizeAddress before it is ever written or
46 // compared, so two spellings of one person cannot become two rows.
47 //
48 // Plaintext, deliberately. The team already knows it, a reminder
49 // email needs it to be deliverable, and the leak list says so.
50 Address string `gorm:"column:address;uniqueIndex;not null"`
51
52 // NameSealed is a display name, if they set one. Sealed: it is
53 // content, and sheets seals the equivalent.
54 NameSealed []byte `gorm:"column:name_sealed"`
55
56 // Zone is the IANA zone this person reads their calendar in
57 // ("Europe/Dublin"). Stated, never sniffed — see the settings page.
58 // A browser's zone is a guess about a device, not a statement about
59 // a person, and a calendar that silently re-times everything
60 // because somebody opened it on holiday is a calendar that lies.
61 Zone string `gorm:"column:zone;not null"`
62
63 // Handle is the plaintext name in this person's public booking
64 // URLs, /book/{handle}/{code}: [a-z0-9-]{2,32}, lowercased on save,
65 // NULL until they choose one. Plaintext because it is in a URL the
66 // person hands to strangers, and unique because two people cannot
67 // share a URL prefix. Nullable so the unique index admits any
68 // number of people who never set one.
69 Handle *string `gorm:"column:handle;uniqueIndex"`
70
71 // WeekStart is 1 for Monday (the default), 7 for Sunday, 6 for
72 // Saturday — time.Weekday's numbering with Sunday as 7 rather than
73 // 0, so "unset" is distinguishable from "Sunday".
74 WeekStart int `gorm:"column:week_start;not null"`
75
76 // The defaults applied to events this person creates. Separate for
77 // timed and all-day because "10 minutes before" is useless for a
78 // birthday and "the evening before" is useless for a standup.
79 DefaultReminderMin int `gorm:"column:default_reminder_min;not null"`
80 DefaultAllDayMin int `gorm:"column:default_all_day_min;not null"`
81
82 CreatedAt time.Time `gorm:"column:created_at;not null"`
83 UpdatedAt time.Time `gorm:"column:updated_at;not null"`
84}
85
86// Calendar is a container of events: one team calendar created with the
87// instance, plus a personal calendar per person.
88type Calendar struct {
89 ID int64 `gorm:"primaryKey"`
90
91 // NameSealed is content, like a sheet's title.
92 NameSealed []byte `gorm:"column:name_sealed"`
93
94 // Kind is "team" or "personal". The team calendar is the one every
95 // person is granted editor on at creation; a personal one is
96 // private until shared.
97 Kind string `gorm:"column:kind;not null"`
98
99 OwnerID int64 `gorm:"column:owner_id;not null;index"`
100
101 // Hue is where this calendar's colour sits on the wheel, 0-360. An
102 // intent rather than a hex value: the theme resolves it to a pair
103 // that clears the contrast floor in both light and dark, which a
104 // stored hex cannot do.
105 //
106 // Colour never carries meaning alone here — every chip prints its
107 // own title and time, and the calendar's name is on the page.
108 Hue int `gorm:"column:hue;not null"`
109
110 CreatedAt time.Time `gorm:"column:created_at;not null"`
111 ArchivedAt *time.Time `gorm:"column:archived_at;index"`
112}
113
114// Grant is the calendar-to-person join, and the one place authorisation
115// is decided.
116//
117// Explicit rows even for the team calendar, where every person gets one
118// at creation. One query shape then serves every calendar, and
119// "a row that isn't yours 404s" has exactly one home rather than a
120// special case that someone will later forget.
121type Grant struct {
122 ID int64 `gorm:"primaryKey"`
123 CalendarID int64 `gorm:"column:calendar_id;not null;uniqueIndex:idx_grant_cal_person"`
124 PersonID int64 `gorm:"column:person_id;not null;uniqueIndex:idx_grant_cal_person"`
125
126 // Role is "owner", "editor" or "viewer".
127 Role string `gorm:"column:role;not null"`
128
129 GrantedBy int64 `gorm:"column:granted_by;not null"`
130 CreatedAt time.Time `gorm:"column:created_at;not null"`
131}
132
133// The roles, as constants, because a role compared against a literal is
134// a role that will eventually be compared against a typo.
135const (
136 RoleOwner = "owner"
137 RoleEditor = "editor"
138 RoleViewer = "viewer"
139)
140
141// CanWrite reports whether a role may change what it names.
142func CanWrite(role string) bool {
143 return role == RoleOwner || role == RoleEditor
144}
145
146// Week start, using time.Weekday's numbering except that Sunday is 7
147// rather than 0 — so a zero column means "never set" and is
148// distinguishable from a deliberate Sunday.
149const (
150 WeekStartMonday = 1
151 WeekStartSaturday = 6
152 WeekStartSunday = 7
153)
154
155// The reminder defaults a new person gets.
156//
157// Ten minutes before a timed event is enough to walk to a room and not
158// so much that it arrives while you are still in the previous thing.
159// An all-day event is reminded the evening before at 18:00 — fifteen
160// hours ahead of its midnight — because a birthday you learn about at
161// midnight is a birthday you have already half-missed.
162const (
163 DefaultReminderMin = 10
164 DefaultAllDayMin = 6 * 60
165)
166
167// Meta is the instance's own key/value scratch: the tzdata stamp the
168// re-derive sweep compares against, and anything else that is a fact
169// about this database rather than about anybody's calendar.
170type Meta struct {
171 Key string `gorm:"primaryKey"`
172 Value string `gorm:"column:value;not null"`
173}
174
175// The Meta keys, spelled once.
176const (
177 MetaTzdata = "tzdata_version"
178)
179