diff options
| author | Calvin Morrison <calvin@pobox.com> | 2026-08-22 16:48:07 -0400 |
|---|---|---|
| committer | Calvin Morrison <calvin@pobox.com> | 2026-08-22 16:48:07 -0400 |
| commit | a071bf9a56241aa743e1be21bccbb4cb72b189f2 (patch) | |
| tree | abb150135648d1cdc220ea21e8bc6ceb90dd0668 /pim/cmd | |
| parent | ebfe721eb065be1d33a13e03e40426c4b3adfad0 (diff) | |
pim: invite, and a way to say what you mean
pim/invite makes an event and, when it names attendees, sends the
invitation -- which on a caldav server with auto-schedule is the same
act. -p writes the object to standard output instead, so it can go to
upas/marshal as iMIP, into a directory an ical/fs is serving, or
nowhere in particular. There is no separate verb for inviting because
to a calendar there is no separate thing.
It writes the tree's own key: value form rather than icalendar. The
first version spelled SUMMARY: by hand and got "lunch, then a walk"
wrong: a comma is a separator there and wants escaping. Parsing with
go-ical and generating by hand is the wrong asymmetry, so calfs grew
Compose, and now the same shape the tree emits is the shape it accepts.
The one UID we ever mint -- as organiser we own the event -- is minted
there too.
pim/invites lists what is waiting for an answer, which is what makes
pim/rsvp usable: it walks the date tree rather than every event, since
an invitation you never answered last March is not news.
agenda and show take -d date now, via seconds(1), which parses a human
date where date(1) only formats one.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Diffstat (limited to 'pim/cmd')
| -rw-r--r-- | pim/cmd/caldavfs/main.go | 5 | ||||
| -rw-r--r-- | pim/cmd/caldavfs/rsvp.go | 44 |
2 files changed, 48 insertions, 1 deletions
diff --git a/pim/cmd/caldavfs/main.go b/pim/cmd/caldavfs/main.go index 7a0a4d3..de37566 100644 --- a/pim/cmd/caldavfs/main.go +++ b/pim/cmd/caldavfs/main.go @@ -67,7 +67,7 @@ func (b *backend) Name() string { return b.name } func (b *backend) Sched() bool { return b.sched } func (b *backend) Caps() string { - c := "read rsvp" + c := "read write rsvp" if b.sched { // the server sends the iMIP for us; one PUT does both halves c += " schedule" @@ -90,6 +90,9 @@ func (b *backend) Status() (time.Time, string) { func (b *backend) Describe() string { s := fmt.Sprintf("collection %s\n", b.path) s += fmt.Sprintf("autoschedule %v\n", b.sched) + if len(b.me) > 0 { + s += "me " + strings.Join(b.me, ",") + "\n" + } return s } diff --git a/pim/cmd/caldavfs/rsvp.go b/pim/cmd/caldavfs/rsvp.go index 47d7425..25015a7 100644 --- a/pim/cmd/caldavfs/rsvp.go +++ b/pim/cmd/caldavfs/rsvp.go @@ -149,3 +149,47 @@ func quoteETag(s string) string { } return `"` + s + `"` } + +// Create adds an event to the collection. +// +// The path is ours to choose, so it is derived from the UID: a server +// that already has that object will replace it, which is what a second +// PUT of the same event should do. With calendar-auto-schedule the +// server mails every ATTENDEE for us, so creating an event with +// attendees is the whole of sending an invitation. +func (b *backend) Create(c *ical.Calendar) error { + var uid string + for _, e := range c.Events() { + if u, err := e.Props.Text(ical.PropUID); err == nil && u != "" { + uid = u + break + } + } + if uid == "" { + return fmt.Errorf("create: the event has no UID") + } + + name := slugUID(uid) + ".ics" + path := strings.TrimSuffix(b.path, "/") + "/" + name + // no etag: this is a create, and If-Match on nothing is meaningless + return b.put(objref{path: path}, c) +} + +// slugUID makes a UID safe as one path element. +func slugUID(s string) string { + var out []rune + for _, r := range s { + switch { + case r >= 'a' && r <= 'z', r >= 'A' && r <= 'Z', r >= '0' && r <= '9': + out = append(out, r) + case r == '-', r == '.', r == '_': + out = append(out, r) + default: + out = append(out, '-') + } + } + if len(out) > 100 { + out = out[:100] + } + return string(out) +} |
