summaryrefslogtreecommitdiff
path: root/pim/lib/cal/compose.go
diff options
context:
space:
mode:
Diffstat (limited to 'pim/lib/cal/compose.go')
-rw-r--r--pim/lib/cal/compose.go112
1 files changed, 112 insertions, 0 deletions
diff --git a/pim/lib/cal/compose.go b/pim/lib/cal/compose.go
new file mode 100644
index 0000000..1960696
--- /dev/null
+++ b/pim/lib/cal/compose.go
@@ -0,0 +1,112 @@
+package cal
+
+import (
+ "bufio"
+ "fmt"
+ "strconv"
+ "strings"
+ "time"
+
+ "github.com/emersion/go-ical"
+)
+
+// Compose builds an event from the same key: value form the tree emits.
+//
+// summary: lunch, then a walk; maybe
+// start: 1787500800
+// end: 1787504400
+// location: the wire
+// attendee: bob@example.com
+// attendee: carol@example.com
+// organizer: you@example.com
+// description: anything
+//
+// Accepting this rather than raw icalendar keeps the escaping where the
+// parser already lives. A shell script writing SUMMARY: by hand has to
+// know that a comma means something, and will eventually forget; here
+// it writes what it means and go-ical spells it.
+//
+// Times are epoch seconds, because that is what date(1) hands a script.
+func Compose(r *bufio.Reader, uid string) (*ical.Calendar, error) {
+ f := map[string][]string{}
+ for {
+ line, err := r.ReadString('\n')
+ if line = strings.TrimRight(line, "\r\n"); line != "" {
+ k, v, ok := strings.Cut(line, ":")
+ if !ok {
+ return nil, fmt.Errorf("compose: %q is not key: value", line)
+ }
+ k = strings.ToLower(strings.TrimSpace(k))
+ f[k] = append(f[k], strings.TrimSpace(v))
+ }
+ if err != nil {
+ break
+ }
+ }
+
+ one := func(k string) string {
+ if v := f[k]; len(v) > 0 {
+ return v[0]
+ }
+ return ""
+ }
+ at := func(k string) (time.Time, error) {
+ v := one(k)
+ if v == "" {
+ return time.Time{}, fmt.Errorf("compose: no %s", k)
+ }
+ n, err := strconv.ParseInt(v, 10, 64)
+ if err != nil {
+ return time.Time{}, fmt.Errorf("compose: %s: want epoch seconds, got %q", k, v)
+ }
+ return time.Unix(n, 0).UTC(), nil
+ }
+
+ if one("summary") == "" {
+ return nil, fmt.Errorf("compose: no summary")
+ }
+ start, err := at("start")
+ if err != nil {
+ return nil, err
+ }
+ end, err := at("end")
+ if err != nil {
+ return nil, err
+ }
+
+ ev := ical.NewEvent()
+ ev.Props.SetText(ical.PropUID, uid)
+ ev.Props.SetDateTime(ical.PropDateTimeStamp, time.Now().UTC())
+ ev.Props.SetDateTime(ical.PropDateTimeStart, start)
+ ev.Props.SetDateTime(ical.PropDateTimeEnd, end)
+ ev.Props.SetText(ical.PropSummary, one("summary"))
+ if v := one("location"); v != "" {
+ ev.Props.SetText(ical.PropLocation, v)
+ }
+ if v := one("description"); v != "" {
+ ev.Props.SetText(ical.PropDescription, v)
+ }
+ if v := one("organizer"); v != "" {
+ p := ical.NewProp(ical.PropOrganizer)
+ p.Value = "mailto:" + v
+ ev.Props.Set(p)
+ // the organiser is attending their own meeting
+ a := ical.NewProp(ical.PropAttendee)
+ a.Value = "mailto:" + v
+ a.Params.Set(ical.ParamParticipationStatus, "ACCEPTED")
+ ev.Props.Add(a)
+ }
+ for _, who := range f["attendee"] {
+ a := ical.NewProp(ical.PropAttendee)
+ a.Value = "mailto:" + who
+ a.Params.Set(ical.ParamParticipationStatus, "NEEDS-ACTION")
+ a.Params.Set("RSVP", "TRUE")
+ ev.Props.Add(a)
+ }
+
+ c := ical.NewCalendar()
+ c.Props.SetText(ical.PropProductID, "-//9front//pim//EN")
+ c.Props.SetText(ical.PropVersion, "2.0")
+ c.Children = append(c.Children, ev.Component)
+ return c, nil
+}