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 "strings"
8)
9
10// Native quick-add uses the same pure parser as the web. The preview's concrete
11// civil dates are saved verbatim, rather than reinterpreting “tomorrow” later.
12type nativeQuickDraft struct {
13 CalendarID int64 `json:"calendar_id"`
14 Title string `json:"title"`
15 Location string `json:"location"`
16 StartLocal string `json:"start_local"`
17 EndLocal string `json:"end_local"`
18 TZ string `json:"tz"`
19 AllDay bool `json:"all_day"`
20 Repeat string `json:"repeat"`
21 Attendees []string `json:"attendees"`
22}
23type nativeQuickPreview struct {
24 Draft nativeQuickDraft `json:"draft"`
25 When string `json:"when"`
26 RepeatLabel string `json:"repeat_label"`
27 CalendarName string `json:"calendar_name"`
28 Assumptions []string `json:"assumptions"`
29}
30
31func decodeNativeQuick(w http.ResponseWriter, r *http.Request, value any) bool {
32 r.Body = http.MaxBytesReader(w, r.Body, 32<<10)
33 d := json.NewDecoder(r.Body)
34 d.DisallowUnknownFields()
35 if d.Decode(value) != nil || d.Decode(new(any)) != io.EOF {
36 http.Error(w, "Invalid quick-add", 400)
37 return false
38 }
39 return true
40}
41func (h *handlers) nativeQuickParse(w http.ResponseWriter, r *http.Request) {
42 var in struct {
43 Text string `json:"text"`
44 Zone string `json:"zone"`
45 }
46 if !decodeNativeQuick(w, r, &in) {
47 return
48 }
49 text := strings.TrimSpace(in.Text)
50 if text == "" || len(text) > 4096 {
51 http.Error(w, "Enter an event in no more than 4096 bytes", 422)
52 return
53 }
54 p, _ := CurrentPerson(r)
55 zone, ok := nativeViewZone(in.Zone, personZone(p))
56 if !ok {
57 http.Error(w, "Choose a valid time zone", 422)
58 return
59 }
60 q := ParseQuick(text, nowIn(zone), zone)
61 id, err := h.store.DefaultCalendarID(r.Context(), p.ID)
62 if err != nil || id == 0 {
63 http.Error(w, "Cannot load default calendar", 500)
64 return
65 }
66 calendars, err := h.store.Calendars(r.Context(), p.ID)
67 if err != nil {
68 http.Error(w, "Cannot load calendars", 500)
69 return
70 }
71 name := ""
72 for _, c := range calendars {
73 if c.ID == id {
74 name = c.Name
75 }
76 }
77 title := q.Title
78 if title == "" {
79 title = "Untitled"
80 }
81 attendees := q.Attendees
82 if attendees == nil {
83 attendees = []string{}
84 }
85 notes := q.Notes
86 if notes == nil {
87 notes = []string{}
88 }
89 pane := quickPaneFor(q, zone)
90 nativeJSON(w, nativeQuickPreview{nativeQuickDraft{id, title, q.Location, q.StartLocal, q.EndLocal, zone, q.AllDay, q.Repeat, attendees}, pane.When, pane.Repeat, name, notes})
91}
92func (h *handlers) nativeQuickSave(w http.ResponseWriter, r *http.Request) {
93 var in nativeQuickDraft
94 if !decodeNativeQuick(w, r, &in) {
95 return
96 }
97 switch in.Repeat {
98 case "", RepeatNone, RepeatDaily, RepeatWeekdays, RepeatWeekly, RepeatFortnight, RepeatMonthly, RepeatYearly:
99 default:
100 http.Error(w, "Unsupported repeat choice", 422)
101 return
102 }
103 if len(in.Attendees) > 100 {
104 http.Error(w, "Too many attendees", 422)
105 return
106 }
107 p, _ := CurrentPerson(r)
108 // CreateEvent remains the grant/validation authority. No calendar ownership,
109 // author identity, status or raw recurrence rule is accepted from the client.
110 ev, err := h.store.CreateEvent(r.Context(), p.ID, EventInput{CalendarID: in.CalendarID, Title: in.Title, Location: in.Location, StartLocal: in.StartLocal, EndLocal: in.EndLocal, TZ: in.TZ, AllDay: in.AllDay, Kind: KindMeeting, Status: StatusConfirmed, RRule: RuleFromRepeat(in.Repeat, "", in.StartLocal, in.TZ, in.AllDay)})
111 if err != nil {
112 nativeEventError(w, r, err)
113 return
114 }
115 warnings := []string{}
116 if len(in.Attendees) > 0 {
117 if err := h.store.SetAttendees(r.Context(), ev.ID, in.Attendees); err != nil {
118 warnings = append(warnings, "Attendees could not be recorded.")
119 }
120 }
121 if err := h.store.EnsureDefaultReminder(r.Context(), p, ev); err != nil {
122 warnings = append(warnings, "Its reminder could not be scheduled.")
123 }
124 result := map[string]any{"id": ev.ID}
125 if len(warnings) > 0 {
126 result["warning"] = "Event saved. " + strings.Join(warnings, " ")
127 }
128 nativeJSON(w, result)
129}
130
131func nativeViewZone(requested, fallback string) (string, bool) {
132 if requested == "" {
133 requested = fallback
134 }
135 if len(requested) > 128 {
136 return "", false
137 }
138 _, err := Zone(requested)
139 return requested, err == nil
140}
141