summaryrefslogtreecommitdiff
path: root/pim/lib/cal/query.go
diff options
context:
space:
mode:
Diffstat (limited to 'pim/lib/cal/query.go')
-rw-r--r--pim/lib/cal/query.go175
1 files changed, 175 insertions, 0 deletions
diff --git a/pim/lib/cal/query.go b/pim/lib/cal/query.go
new file mode 100644
index 0000000..325c9a0
--- /dev/null
+++ b/pim/lib/cal/query.go
@@ -0,0 +1,175 @@
+package cal
+
+import (
+ "fmt"
+ "strings"
+ "sync"
+ "time"
+
+ "github.com/knusbaum/go9p/fs"
+ "github.com/knusbaum/go9p/proto"
+)
+
+// The query file works like /net/cs: open it, write a query, read the
+// answer back on the same fd.
+//
+// % echo 'attendee=joe@example.com' >/mnt/pim/query
+// % cat /mnt/pim/query
+//
+// A query is ndb-style attr=value pairs, all of which must match:
+//
+// summary= substring of the summary, case-insensitive
+// attendee= substring of any attendee's name or address
+// organizer= substring of the organizer
+// location= substring of the location
+// uid= substring of the uid
+// from= YYYY-MM-DD, occurrences on or after this day
+// to= YYYY-MM-DD, occurrences before this day
+//
+// It answers with one path per line, which is what pim/show takes.
+// Holding the fd across the write and the read is the correct way to
+// use it, as with cs. But "echo ... >query; cat query" opens twice, and
+// that is how people will actually use it from rc, so the last answer is
+// also kept and served to a fid that has none of its own.
+type queryFile struct {
+ mu sync.Mutex
+ res map[uint64][]byte
+ last []byte
+}
+
+// index is one occurrence and the path it was published at.
+type index struct {
+ path string
+ in Instance
+}
+
+func (s *Server) addQuery() {
+ q := &queryFile{res: make(map[uint64][]byte)}
+ st := s.fsys.NewStat("query", s.user, s.user, 0666)
+ base := fs.NewStaticFile(st, []byte(""))
+ s.root.AddChild(&fs.WrappedFile{
+ File: base,
+ WriteF: func(fid uint64, off uint64, data []byte) (uint32, error) {
+ out, err := s.query(string(data))
+ if err != nil {
+ return 0, err
+ }
+ q.mu.Lock()
+ q.res[fid] = []byte(out)
+ q.last = []byte(out)
+ q.mu.Unlock()
+ return uint32(len(data)), nil
+ },
+ ReadF: func(fid uint64, off uint64, count uint64) ([]byte, error) {
+ q.mu.Lock()
+ b, ok := q.res[fid]
+ if !ok {
+ b = q.last
+ }
+ q.mu.Unlock()
+ if off >= uint64(len(b)) {
+ return []byte{}, nil
+ }
+ end := off + count
+ if end > uint64(len(b)) {
+ end = uint64(len(b))
+ }
+ return b[off:end], nil
+ },
+ CloseF: func(fid uint64) error {
+ q.mu.Lock()
+ delete(q.res, fid)
+ q.mu.Unlock()
+ return nil
+ },
+ })
+}
+
+func (s *Server) query(q string) (string, error) {
+ var from, to time.Time
+ terms := map[string]string{}
+
+ for _, f := range strings.Fields(strings.TrimSpace(q)) {
+ k, v, ok := strings.Cut(f, "=")
+ if !ok {
+ return "", fmt.Errorf("query: %q is not attr=value", f)
+ }
+ switch k {
+ case "summary", "attendee", "organizer", "location", "uid":
+ terms[k] = strings.ToLower(v)
+ case "from", "to":
+ t, err := time.ParseInLocation("2006-01-02", v, time.Local)
+ if err != nil {
+ return "", fmt.Errorf("query: bad date %q", v)
+ }
+ if k == "from" {
+ from = t
+ } else {
+ to = t
+ }
+ default:
+ return "", fmt.Errorf("query: unknown attribute %q", k)
+ }
+ }
+ if len(terms) == 0 && from.IsZero() && to.IsZero() {
+ return "", fmt.Errorf("query: nothing to match")
+ }
+
+ s.mu.Lock()
+ idx := s.index
+ s.mu.Unlock()
+
+ var b strings.Builder
+ for _, e := range idx {
+ if !from.IsZero() && e.in.Start.Before(from) {
+ continue
+ }
+ if !to.IsZero() && !e.in.Start.Before(to) {
+ continue
+ }
+ if match(e.in.Ev, terms) {
+ fmt.Fprintf(&b, "%s\n", e.path)
+ }
+ }
+ return b.String(), nil
+}
+
+func match(ev *Event, terms map[string]string) bool {
+ has := func(hay, needle string) bool {
+ return strings.Contains(strings.ToLower(hay), needle)
+ }
+ for k, v := range terms {
+ switch k {
+ case "summary":
+ if !has(ev.Summary, v) {
+ return false
+ }
+ case "location":
+ if !has(ev.Location, v) {
+ return false
+ }
+ case "organizer":
+ if !has(ev.Organizer, v) {
+ return false
+ }
+ case "uid":
+ if !has(ev.UID, v) {
+ return false
+ }
+ case "attendee":
+ found := false
+ for _, a := range ev.Attendees {
+ if has(a.Name, v) || has(a.Email, v) {
+ found = true
+ break
+ }
+ }
+ if !found {
+ return false
+ }
+ }
+ }
+ return true
+}
+
+var _ = proto.DMDIR