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 "encoding/json"
5 "io"
6 "net/http"
7 "strconv"
8 "time"
9
10 nativeauth "amadan.net/rastrillo/native/server"
11 "github.com/go-chi/chi/v5"
12
13 // The one import left on rastrillo's pre-move path, on purpose:
14 // rastrillo/native's server (2a5d07b, and its main) still takes a
15 // github.com/carlosframework/rastrillo *sessions.Sessions. Both
16 // sessions packages read and write the same sessions table, so a
17 // session native sign-in mints is an ordinary session to the app
18 // (native_exchange_test.go). Move this to
19 // amadan.net/rastrillo/rastrillo/sessions when native/server moves.
20 presessions "github.com/carlosframework/rastrillo/sessions"
21)
22
23func (a *App) wireNative() error {
24 s, err := presessions.New(presessions.Config{DB: a.db.Writer(), Origin: a.cfg.Origin, SigninPath: "/signin", Logger: a.logger})
25 if err != nil {
26 return err
27 }
28 require := func(next http.Handler) http.Handler {
29 if a.home != nil {
30 return a.home.sessions.Require(a.requirePerson(next))
31 }
32 return a.auth.RequireSession(a.requirePerson(next))
33 }
34 a.native = nativeauth.New(s, a.cfg.Origin, "Calendar", require)
35 return nil
36}
37func nativeJSON(w http.ResponseWriter, value any) {
38 w.Header().Set("Content-Type", "application/json")
39 w.Header().Set("Cache-Control", "no-store")
40 _ = json.NewEncoder(w).Encode(value)
41}
42func (h *handlers) nativeSession(w http.ResponseWriter, r *http.Request) {
43 p, _ := CurrentPerson(r)
44 cals, err := h.store.Calendars(r.Context(), p.ID)
45 if err != nil {
46 http.Error(w, "Cannot load calendars", 500)
47 return
48 }
49 type cal struct {
50 ID int64 `json:"id"`
51 Name string `json:"name"`
52 Hue int `json:"hue"`
53 Writable bool `json:"writable"`
54 }
55 out := make([]cal, 0, len(cals))
56 for _, c := range cals {
57 out = append(out, cal{c.ID, c.Name, c.Hue, CanWrite(c.Role)})
58 }
59 id, err := h.store.DefaultCalendarID(r.Context(), p.ID)
60 if err != nil {
61 http.Error(w, "Cannot load default calendar", 500)
62 return
63 }
64 nativeJSON(w, map[string]any{"zone": p.Zone, "week_start": p.WeekStart, "calendars": out, "default_calendar_id": id})
65}
66
67type nativeEvent struct {
68 ID int64 `json:"id"`
69 CalendarID int64 `json:"calendar_id"`
70 Title string `json:"title"`
71 Location string `json:"location"`
72 Notes string `json:"notes"`
73 AllDay bool `json:"all_day"`
74 StartLocal string `json:"start_local"`
75 EndLocal string `json:"end_local"`
76 TZ string `json:"tz"`
77 Start int64 `json:"start"`
78 End int64 `json:"end"`
79 Recurring bool `json:"recurring"`
80 Writable bool `json:"writable"`
81 CalendarName string `json:"calendar_name"`
82 Occurrence string `json:"occurrence"`
83 DayStart string `json:"day_start"`
84 DayEnd string `json:"day_end"`
85 Status string `json:"status"`
86}
87
88func (h *handlers) nativeEvents(w http.ResponseWriter, r *http.Request) {
89 p, _ := CurrentPerson(r)
90 zone, ok := nativeViewZone(r.URL.Query().Get("zone"), personZone(p))
91 if !ok {
92 http.Error(w, "Choose a valid time zone", 422)
93 return
94 }
95 from, e := ParseDate(r.URL.Query().Get("from"), zone)
96 to, e2 := ParseDate(r.URL.Query().Get("to"), zone)
97 if e != nil || e2 != nil || !to.After(from) || to.After(from.AddDate(0, 0, 62)) {
98 http.Error(w, "Choose a range of at most 62 days", 400)
99 return
100 }
101 events, err := h.store.EventsInRange(r.Context(), p.ID, from.AddDate(0, 0, -2).Unix(), to.AddDate(0, 0, 2).Unix())
102 if err != nil {
103 http.Error(w, "Cannot load events", 500)
104 return
105 }
106 out := make([]nativeEvent, 0, len(events))
107 for _, ev := range events {
108 // The first editor handles single, non-booked events; series remain intact.
109 _, booked := h.store.BookingSummaryFor(r.Context(), ev.ID)
110 dayStart, dayEnd := "", ""
111 if ev.AllDay {
112 dayStart = ev.StartLocal
113 if ev.RRule != "" && ev.SeriesID == nil && ev.OccurrenceLocal != "" {
114 dayStart = ev.OccurrenceLocal
115 }
116 masterStart, e1 := time.Parse(LayoutDate, ev.StartLocal)
117 masterEnd, e2 := time.Parse(LayoutDate, ev.EndLocal)
118 occurrenceStart, e3 := time.Parse(LayoutDate, dayStart)
119 if e1 != nil || e2 != nil || e3 != nil {
120 http.Error(w, "Cannot load dates", 500)
121 return
122 }
123 dayEnd = occurrenceStart.Add(masterEnd.Sub(masterStart)).Format(LayoutDate)
124 if dayEnd <= r.URL.Query().Get("from") || dayStart >= r.URL.Query().Get("to") {
125 continue
126 }
127 } else if ev.OccEndUTC <= from.Unix() || ev.OccStartUTC >= to.Unix() {
128 continue
129 }
130 recurring := ev.RRule != "" || ev.SeriesID != nil
131 out = append(out, nativeEvent{ev.ID, ev.CalendarID, ev.Title, ev.Location, ev.Notes, ev.AllDay, ev.StartLocal, ev.EndLocal, ev.TZ, ev.OccStartUTC, ev.OccEndUTC, recurring, ev.Writable() && !recurring && !booked, ev.CalendarName, ev.OccurrenceLocal, dayStart, dayEnd, ev.Status})
132 }
133 nativeJSON(w, map[string]any{"events": out})
134}
135func (h *handlers) nativeSave(w http.ResponseWriter, r *http.Request) {
136 p, _ := CurrentPerson(r)
137 r.Body = http.MaxBytesReader(w, r.Body, 128<<10)
138 var in nativeEvent
139 d := json.NewDecoder(r.Body)
140 d.DisallowUnknownFields()
141 if err := d.Decode(&in); err != nil || d.Decode(new(any)) != io.EOF {
142 http.Error(w, "Invalid event", 400)
143 return
144 }
145 id := int64(0)
146 if raw := chi.URLParam(r, "id"); raw != "" {
147 var err error
148 id, err = strconv.ParseInt(raw, 10, 64)
149 if err != nil || id <= 0 {
150 http.NotFound(w, r)
151 return
152 }
153 }
154 value := EventInput{CalendarID: in.CalendarID, Title: in.Title, Location: in.Location, Notes: in.Notes, AllDay: in.AllDay, StartLocal: in.StartLocal, EndLocal: in.EndLocal, TZ: in.TZ, Kind: KindMeeting, Status: StatusConfirmed}
155 if value.TZ == "" {
156 value.TZ = p.Zone
157 }
158 if id != 0 {
159 ev, err := h.store.Event(r.Context(), p.ID, id)
160 if err != nil {
161 http.NotFound(w, r)
162 return
163 }
164 _, booked := h.store.BookingSummaryFor(r.Context(), id)
165 if ev.RRule != "" || ev.SeriesID != nil || booked {
166 http.Error(w, "Edit this series or booking in the web calendar", 409)
167 return
168 }
169 value.CalendarID = ev.CalendarID
170 value.Kind = ev.Kind
171 value.Status = ev.Status
172 if err = h.store.UpdateEvent(r.Context(), p.ID, id, value); err != nil {
173 nativeEventError(w, r, err)
174 return
175 }
176 } else {
177 ev, err := h.store.CreateEvent(r.Context(), p.ID, value)
178 if err != nil {
179 nativeEventError(w, r, err)
180 return
181 }
182 id = ev.ID
183 if err := h.store.EnsureDefaultReminder(r.Context(), p, ev); err != nil {
184 nativeJSON(w, map[string]any{"id": id, "warning": "Event saved, but its reminder could not be scheduled."})
185 return
186 }
187 }
188 nativeJSON(w, map[string]any{"id": id, "saved_at": time.Now().Unix()})
189}
190func nativeEventError(w http.ResponseWriter, r *http.Request, err error) {
191 if NotFound(err) {
192 http.NotFound(w, r)
193 return
194 }
195 if err == ErrReadOnly {
196 http.Error(w, "This calendar is read-only", 403)
197 return
198 }
199 if v, ok := Invalid(err); ok {
200 http.Error(w, v.Message, 422)
201 return
202 }
203 http.Error(w, "Cannot save event", 500)
204}
205