summaryrefslogtreecommitdiff
path: root/pim/lib/cal/compose.go
blob: 1960696164c9965348842cba393fb64c08f28ec8 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
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
}