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 }