oficina / meet Public

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

Plain git — no account needed to clone.

Download

Download this file

1package calls
2
3import "time"
4
5// Models is every model the schema generator manages. Keep it in step
6// with the structs below: `rastrillo migration generate` reads it, and
7// `rastrillo migration check` fails CI when it and the migrations
8// disagree.
9var Models = []any{
10 &Person{},
11 &Meeting{},
12 &MeetingLink{},
13 &GuestGrant{},
14 &PendingJoin{},
15 &Agent{},
16 &MeetingDefaults{},
17 &DeviceToken{},
18}
19
20// Person is a verified identity or an anonymous guest. Anonymous guests carry
21// an opaque guest: identifier in Email, never a claimed or verified address.
22// Standing (member, guest, none) is computed per request, never stored.
23type Person struct {
24 Anonymous bool `gorm:"not null;default:false"`
25 ID int64
26 Email string `gorm:"uniqueIndex;not null"`
27 Name string `gorm:"not null"`
28 CreatedAt time.Time
29}
30
31// TableName overrides GORM's default pluralization. GORM treats
32// "person" as an irregular noun (via jinzhu/inflection) and would
33// otherwise name this table "people"; the spec and this app's schema
34// test both name it "persons".
35func (Person) TableName() string { return "persons" }
36
37// Meeting is a room that may be entered. Its title is sealed; its
38// times are plaintext because the list sorts on them.
39//
40// AutoTitle says the title is one Calls wrote ("Call, 2 Oct 15:40",
41// callTitle) rather than one a person chose: the apps show a chosen
42// title over the call and leave a written one off (2026-10-02). It is a
43// flag set where the title is made, not something read back out of the
44// title's words. TitleZone is the IANA zone an automatic title's time
45// is in — the starter's when their browser or app said, otherwise the
46// instance's — kept as data so the title itself can read cleanly
47// without naming it.
48type Meeting struct {
49 RequireSignin bool `gorm:"not null;default:false"`
50 WaitingRoom bool `gorm:"not null;default:false"`
51 AutoTitle bool `gorm:"not null;default:false"`
52 TitleZone string `gorm:"not null;default:''"`
53 ID int64
54 Sqid string `gorm:"uniqueIndex;not null"`
55 TitleSealed []byte `gorm:"column:title_sealed"`
56 StartsAt *time.Time
57 CreatedBy string `gorm:"not null"`
58 CreatedAt time.Time
59 EndedAt *time.Time
60 EndedBy string
61}
62
63// MeetingLink looks up invitations by hash. New tokens are also sealed so
64// members can share the current invitation again; older rows remain hash-only.
65type MeetingLink struct {
66 ID int64
67 MeetingID int64 `gorm:"index;not null"`
68 TokenHash string `gorm:"uniqueIndex;not null"`
69 TokenSealed []byte
70 CreatedBy string `gorm:"not null"`
71 CreatedAt time.Time
72 RevokedAt *time.Time
73}
74
75// DeviceToken is one Apple device a person can be reached on when the
76// app is not running. It is the only part of the knock notification that
77// has to be stored: the queue itself lives in the hub's memory and dies
78// with the process, which is exactly why a person outside the room could
79// never be told about it.
80//
81// The token is stored in plaintext, unlike a meeting title. It is not a
82// secret about a person — it is an address Apple issues, scoped to one
83// app on one device, useless without the push key, and rotated by the
84// system whenever it likes. Sealing it would cost the sweep the ability
85// to delete by token when Apple reports one unregistered, which is the
86// one hygiene operation this table needs.
87//
88// Unique on the token rather than on (person, token): Apple reissues a
89// token to whoever installs the app next, so the same string arriving
90// for a different person means the device changed hands and the old row
91// is wrong, not that there are two.
92type DeviceToken struct {
93 ID int64
94 PersonID int64 `gorm:"index;not null"`
95 Token string `gorm:"uniqueIndex;not null"`
96 // "ios" or "macos". Apple keeps one token space per app per device,
97 // but the two apps have different bundle identifiers and therefore
98 // different APNs topics, so a token is useless without knowing which.
99 Platform string `gorm:"not null"`
100 CreatedAt time.Time
101 LastSeen time.Time
102}
103
104// GuestGrant is a non-member's entry authority for one meeting. One
105// row per person per meeting. AdmittedAt and AdmittedBy are an audit of
106// the first admission (slice 2) and confer nothing.
107type GuestGrant struct {
108 ID int64
109 MeetingID int64 `gorm:"uniqueIndex:idx_grant_meeting_person;not null"`
110 PersonID int64 `gorm:"uniqueIndex:idx_grant_meeting_person;not null"`
111 GrantedAt time.Time
112 ExpiresAt time.Time `gorm:"index"`
113 AdmittedAt *time.Time
114 AdmittedBy string
115}
116
117// PendingJoin is written before a magic link is mailed to someone who
118// followed a live meeting link, so the emailed link can admit them in
119// any browser and /continue can find their destination. The address is
120// a keyed fingerprint, never plaintext.
121type PendingJoin struct {
122 ID int64
123 MeetingID int64 `gorm:"not null"`
124 LinkID int64 `gorm:"not null"`
125 AddressFingerprint string `gorm:"index;not null"`
126 CreatedAt time.Time
127 ExpiresAt time.Time
128}
129
130// Agent is a drive-surface principal (spec §8.1): a bearer token minted
131// by a member (the operator) that reaches exactly as far as that
132// member's own current standing, and no further once they leave. The
133// token itself is never stored — only the SHA-256 of it.
134type Agent struct {
135 ID int64
136 Name string `gorm:"not null"`
137 OperatorPersonID int64 `gorm:"index;not null"`
138 TokenHash string `gorm:"uniqueIndex;not null"`
139 CreatedAt time.Time
140 RevokedAt *time.Time
141}
142
143// MeetingDefaults holds the team’s defaults for newly created meetings.
144type MeetingDefaults struct {
145 ID int64
146 RequireSignin bool `gorm:"not null;default:false"`
147 WaitingRoom bool `gorm:"not null;default:false"`
148}
149