diff options
Diffstat (limited to 'pim/lib/cal')
| -rw-r--r-- | pim/lib/cal/backend.go | 23 | ||||
| -rw-r--r-- | pim/lib/cal/ical.go | 26 | ||||
| -rw-r--r-- | pim/lib/cal/tree.go | 58 |
3 files changed, 101 insertions, 6 deletions
diff --git a/pim/lib/cal/backend.go b/pim/lib/cal/backend.go index 6dcacc2..6ed488c 100644 --- a/pim/lib/cal/backend.go +++ b/pim/lib/cal/backend.go @@ -39,3 +39,26 @@ type Backend interface { type Describer interface { Describe() string } + +// Identity is implemented by a backend that knows whose calendar this +// is. In iTIP your identity is the mailto: in your own ATTENDEE line -- +// there is no separate field for it -- so without this nothing can tell +// which of an event's attendees is you. Aliases are listed because you +// may be invited at one address and reply from another. +type Identity interface { + Me() []string +} + +// RSVPer is implemented by a backend that can answer an invitation. +// +// Accepting is two writes: update your own copy, and tell the organiser. +// A CalDAV server advertising calendar-auto-schedule does both from one +// PUT. A backend that cannot do both should not implement this at all, +// so that writing to partstat fails honestly rather than doing half of +// it and looking like it worked. +// +// recurID is the zero time to answer a whole series, or the start of +// one occurrence to answer only that. +type RSVPer interface { + RSVP(uid string, recurID time.Time, partstat string) error +} diff --git a/pim/lib/cal/ical.go b/pim/lib/cal/ical.go index f52b333..5477030 100644 --- a/pim/lib/cal/ical.go +++ b/pim/lib/cal/ical.go @@ -95,29 +95,43 @@ func LoadDir(dir string) ([]*Event, error) { return nil, err } sort.Strings(names) - var evs []*Event + var cals []*ical.Calendar for _, name := range names { f, err := os.Open(name) if err != nil { Warnf("%s: %v", name, err) continue } - cal, err := ical.NewDecoder(f).Decode() + c, err := ical.NewDecoder(f).Decode() f.Close() if err != nil { Warnf("%s: %v", name, err) continue } - for _, c := range cal.Events() { - ev, err := newEvent(&c) + cals = append(cals, c) + } + return FromCalendars(cals), nil +} + +// FromCalendars turns decoded iCalendar objects into events, with +// RECURRENCE-ID overrides attached to the series they belong to. +// +// A backend that already holds parsed calendars -- CalDAV hands them +// straight back from a REPORT -- comes through here rather than writing +// them to disk first. +func FromCalendars(cals []*ical.Calendar) []*Event { + var evs []*Event + for _, c := range cals { + for _, e := range c.Events() { + ev, err := newEvent(&e) if err != nil { - Warnf("%s: %v", name, err) + Warnf("%v", err) continue } evs = append(evs, ev) } } - return link(evs), nil + return link(evs) } // link attaches RECURRENCE-ID events to the series they override. diff --git a/pim/lib/cal/tree.go b/pim/lib/cal/tree.go index cd9e9d2..f78f2eb 100644 --- a/pim/lib/cal/tree.go +++ b/pim/lib/cal/tree.go @@ -112,6 +112,7 @@ func (s *Server) buildEvents(root *fs.StaticDir, evs []*Event) { s.file(ed, "attendees", attendeeText(e.Attendees)) } s.file(ed, "uid", e.UID+"\n") + s.addPartstat(ed, e) s.file(ed, "raw", e.Raw) } } @@ -215,3 +216,60 @@ func attendeeText(as []Attendee) string { } return b.String() } + +// addPartstat publishes your own reply status for an event. +// +// Reading it says where you stand. Writing it answers the invitation -- +// but only if the backend can actually do both halves of that: update +// the calendar and tell the organiser. A published .ics can do neither, +// so there the file is read-only and says why. +func (s *Server) addPartstat(dir *fs.StaticDir, e *Event) { + var me []string + if id, ok := s.be.(Identity); ok { + me = id.Me() + } + cur := "" // no identity, or not an attendee + for _, a := range e.Attendees { + for _, m := range me { + if strings.EqualFold(a.Email, m) || strings.EqualFold(a.Name, m) { + cur = a.Partstat + } + } + } + if cur == "" && len(me) == 0 { + cur = "unknown" + } else if cur == "" { + cur = "not-an-attendee" + } + + rsvp, canWrite := s.be.(RSVPer) + mode := uint32(0444) + if canWrite { + mode = 0666 + } + st := s.fsys.NewStat("partstat", s.user, s.user, mode) + base := fs.NewStaticFile(st, []byte(cur+"\n")) + if !canWrite { + dir.AddChild(base) + return + } + uid := e.UID + dir.AddChild(&fs.WrappedFile{ + File: base, + WriteF: func(fid uint64, off uint64, data []byte) (uint32, error) { + want := strings.ToUpper(strings.TrimSpace(string(data))) + switch want { + case "ACCEPTED", "DECLINED", "TENTATIVE": + default: + return 0, fmt.Errorf("partstat: want ACCEPTED, DECLINED or TENTATIVE") + } + if len(me) == 0 { + return 0, fmt.Errorf("partstat: no me= for this calendar") + } + if err := rsvp.RSVP(uid, time.Time{}, want); err != nil { + return 0, err + } + return uint32(len(data)), nil + }, + }) +} |
