| 1 | package calendar |
| 2 | |
| 3 | import ( |
| 4 | "strconv" |
| 5 | "time" |
| 6 | ) |
| 7 | |
| 8 | // publicBooking is EVERYTHING a public page, a booker's mail or the ICS |
| 9 | // may know about a booking (booking spec §5.5). Built from a |
| 10 | // BookingView by exactly one function, and public templates receive |
| 11 | // this and nothing else — never a BookingView, never an Event — so the |
| 12 | // host's notes, location and attendee list cannot reach a stranger by |
| 13 | // accident. |
| 14 | type publicBooking struct { |
| 15 | Title, Description string |
| 16 | |
| 17 | // The event's times, as instants and as wall clock in the booker's |
| 18 | // zone (When) and the host's (HostWhen) when they differ. |
| 19 | StartUTC, EndUTC int64 |
| 20 | When, HostWhen string |
| 21 | Zone, HostZone string |
| 22 | Rev int |
| 23 | |
| 24 | HostName, HostAddress string |
| 25 | Name, Address string |
| 26 | |
| 27 | State string |
| 28 | CancelledBy string |
| 29 | Live bool // confirmed and the event stands |
| 30 | Held bool // held and not lapsed |
| 31 | Expired bool |
| 32 | Cancelled bool |
| 33 | ExpiresAt string // booker zone, for a held booking |
| 34 | |
| 35 | // Token is set only on a page that already knows it (it came in |
| 36 | // the URL), for building the links on that page. |
| 37 | Token string |
| 38 | } |
| 39 | |
| 40 | // publicOf is the one door. now decides whether a hold has lapsed. |
| 41 | func publicOf(v BookingView, now time.Time) publicBooking { |
| 42 | pb := publicBooking{ |
| 43 | Title: v.Title, Description: v.Description, |
| 44 | StartUTC: v.Event.StartUTC, EndUTC: v.Event.EndUTC, |
| 45 | Zone: v.BookerZone, HostZone: v.Event.TZ, Rev: v.Event.Sequence, |
| 46 | HostName: v.HostName, HostAddress: v.Host.Address, |
| 47 | Name: v.Name, Address: v.Address, |
| 48 | State: v.State, CancelledBy: v.CancelledBy, |
| 49 | } |
| 50 | pb.When = spanWords(v.Event.StartUTC, v.Event.EndUTC, v.BookerZone) |
| 51 | if v.Event.TZ != "" && v.Event.TZ != v.BookerZone { |
| 52 | pb.HostWhen = spanWords(v.Event.StartUTC, v.Event.EndUTC, v.Event.TZ) |
| 53 | } |
| 54 | pb.Cancelled = v.Cancelled() |
| 55 | pb.Live = v.Live() |
| 56 | switch v.State { |
| 57 | case BookingHeld: |
| 58 | if now.Unix() < v.HoldExpiresAt && !pb.Cancelled { |
| 59 | pb.Held = true |
| 60 | } else { |
| 61 | pb.Expired = true |
| 62 | } |
| 63 | pb.ExpiresAt = instantWords(v.HoldExpiresAt, v.BookerZone) |
| 64 | case BookingExpired: |
| 65 | pb.Expired = true |
| 66 | pb.ExpiresAt = instantWords(v.HoldExpiresAt, v.BookerZone) |
| 67 | } |
| 68 | if pb.CancelledBy == "" && pb.Cancelled { |
| 69 | pb.CancelledBy = CancelledByHost |
| 70 | } |
| 71 | return pb |
| 72 | } |
| 73 | |
| 74 | // spanWords is "Monday 7 September 2026, 09:00–09:30 (Europe/Dublin)". |
| 75 | func spanWords(startUTC, endUTC int64, zone string) string { |
| 76 | s, err := InZone(startUTC, zone) |
| 77 | if err != nil { |
| 78 | return "" |
| 79 | } |
| 80 | e, err := InZone(endUTC, zone) |
| 81 | if err != nil { |
| 82 | return "" |
| 83 | } |
| 84 | out := s.Format("Monday 2 January 2006, 15:04") |
| 85 | if e.Format("2006-01-02") == s.Format("2006-01-02") { |
| 86 | out += "–" + e.Format("15:04") |
| 87 | } else { |
| 88 | out += " – " + e.Format("Monday 2 January 2006, 15:04") |
| 89 | } |
| 90 | return out + " (" + zone + ")" |
| 91 | } |
| 92 | |
| 93 | func instantWords(unix int64, zone string) string { |
| 94 | t, err := InZone(unix, zone) |
| 95 | if err != nil { |
| 96 | return "" |
| 97 | } |
| 98 | return t.Format("15:04 on Monday 2 January") + " (" + zone + ")" |
| 99 | } |
| 100 | |
| 101 | // Link helpers, so templates never assemble a URL. |
| 102 | func (h *handlers) manageURL(token string) string { return h.origin + "/book/m/" + token } |
| 103 | func (h *handlers) eventURL(id int64) string { |
| 104 | return h.origin + "/events/" + strconv.FormatInt(id, 10) |
| 105 | } |
| 106 | |