| 1 | package calls |
| 2 | |
| 3 | import "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. |
| 9 | var 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. |
| 23 | type 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". |
| 35 | func (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. |
| 48 | type 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. |
| 65 | type 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. |
| 92 | type 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. |
| 107 | type 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. |
| 121 | type 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. |
| 134 | type 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. |
| 144 | type MeetingDefaults struct { |
| 145 | ID int64 |
| 146 | RequireSignin bool `gorm:"not null;default:false"` |
| 147 | WaitingRoom bool `gorm:"not null;default:false"` |
| 148 | } |
| 149 | |