summaryrefslogtreecommitdiff
path: root/pim/rc
diff options
context:
space:
mode:
Diffstat (limited to 'pim/rc')
-rwxr-xr-xpim/rc/agenda15
-rwxr-xr-xpim/rc/invite137
-rwxr-xr-xpim/rc/invites87
-rwxr-xr-xpim/rc/rsvp111
-rwxr-xr-xpim/rc/show16
5 files changed, 361 insertions, 5 deletions
diff --git a/pim/rc/agenda b/pim/rc/agenda
index ec38ad2..563758f 100755
--- a/pim/rc/agenda
+++ b/pim/rc/agenda
@@ -6,6 +6,7 @@ mtpt=/mnt/pim
days=7
off=0
only=()
+start=()
while(~ $1 -*){
switch($1){
@@ -15,10 +16,18 @@ while(~ $1 -*){
days=$2; shift
case -o
off=$2; shift
+ case -d
+ # seconds(1) parses a human date; date(1) only formats one
+ start=`{seconds $2}
+ if(~ $#start 0){
+ echo 'pim/agenda: cannot read the date '^$2 >[1=2]
+ exit baddate
+ }
+ shift
case -c
only=($only $2); shift
case *
- echo 'usage: agenda [-m mtpt] [-n days] [-o dayoffset] [-c cal]' >[1=2]
+ echo 'usage: agenda [-m mtpt] [-n days] [-d date] [-o dayoffset] [-c cal]' >[1=2]
exit usage
}
shift
@@ -39,7 +48,9 @@ if(! ~ $#cals 1)
tmp=/tmp/agenda.$pid
fn sigexit { rm -f $tmp }
-now=`{date -n}
+now=$start
+if(~ $#now 0)
+ now=`{date -n}
i=$off
last=`{echo $off + $days | bc}
n=0
diff --git a/pim/rc/invite b/pim/rc/invite
new file mode 100755
index 0000000..cf2e69c
--- /dev/null
+++ b/pim/rc/invite
@@ -0,0 +1,137 @@
+#!/bin/rc
+# pim/invite -- make an event, and invite people to it.
+#
+# pim/invite -c fastmail 'catch up' '2026-08-28 15:00' 30
+# pim/invite -c fastmail -a bob@example.com -l 'the wire' \
+# 'design review' 'tomorrow 14:00' 60
+#
+# What this really does is write an icalendar object. Where it goes is
+# somebody else's business:
+#
+# -p writes it to standard output, so you can mail it yourself
+# (upas/marshal -t 'text/calendar; method=REQUEST'), keep it,
+# or drop it in a directory some ical/fs is serving
+# otherwise it goes to the calendar's new file, and a backend with
+# calendar-auto-schedule mails every attendee for you
+#
+# So there is no separate verb for "invite": to a calendar there is no
+# separate thing, and to everything else it is just a file.
+rfork e
+
+mtpt=/mnt/pim
+print=()
+cal=()
+att=()
+loc=()
+desc=()
+
+while(~ $1 -*){
+ switch($1){
+ case -m
+ mtpt=$2; shift
+ case -c
+ cal=$2; shift
+ case -a
+ att=($att $2); shift
+ case -l
+ loc=$2; shift
+ case -D
+ desc=$2; shift
+ case -p
+ print=1
+ case *
+ echo 'usage: invite [-m mtpt] [-c cal] [-a attendee]... [-l where] [-D text] [-p] summary start [minutes]' >[1=2]
+ exit usage
+ }
+ shift
+}
+if(test $#* -lt 2){
+ echo 'usage: invite [-m mtpt] [-c cal] [-a attendee]... [-l where] [-D text] summary start [minutes]' >[1=2]
+ exit usage
+}
+summary=$1
+when=$2
+mins=30
+if(! ~ $#* 2)
+ mins=$3
+
+# with -p the object goes to standard output and no calendar is needed
+if(! ~ $#print 0){
+ if(~ $#cal 0)
+ cal=`{pim/calendars -m $mtpt | sed 1q}
+}
+if(~ $#print 0 && ~ $#cal 0){
+ for(c in `{pim/calendars -m $mtpt}){
+ caps=`{sed -n 's/^caps //p' $mtpt/calendars/$c/ctl >[2]/dev/null}
+ for(x in $caps)
+ if(~ $x write)
+ cal=($cal $c)
+ }
+ if(~ $#cal 0){
+ echo 'pim/invite: no calendar here can create events' >[1=2]
+ exit nowhere
+ }
+ if(! ~ $#cal 1){
+ echo 'pim/invite: which calendar? -c one of: '^$"cal >[1=2]
+ exit ambiguous
+ }
+}
+d=$mtpt/calendars/$"cal
+if(~ $#print 0)
+ if(! test -f $d/new){
+ echo 'pim/invite: '^$"cal^' cannot create events' >[1=2]
+ exit readonly
+ }
+
+start=`{seconds $"when}
+if(~ $#start 0){
+ echo 'pim/invite: cannot read the time '^$"when >[1=2]
+ exit baddate
+}
+# organiser is whoever this calendar says we are
+me=`{sed -n 's/^me //p' $d/ctl >[2]/dev/null | sed 's/,.*//'}
+if(~ $#att 0)
+ me=()
+if(! ~ $#att 0)
+ if(~ $#me 0){
+ echo 'pim/invite: '^$"cal^' has no me=; cannot say who is organising' >[1=2]
+ exit nome
+ }
+
+# The event is written as key: value, the same form the tree emits and
+# calfs composes from. A script spelling SUMMARY: by hand has to know
+# that a comma means something there, and will eventually forget; this
+# way go-ical does the spelling and the escaping.
+end=`{echo $"start + $"mins '*' 60 | bc}
+
+tmp=/tmp/invite.$pid
+fn sigexit { rm -f $tmp }
+{
+ echo 'summary: '^$"summary
+ echo 'start: '^$"start
+ echo 'end: '^$"end
+ if(! ~ $#loc 0)
+ echo 'location: '^$"loc
+ if(! ~ $#desc 0)
+ echo 'description: '^$"desc
+ if(! ~ $#att 0){
+ echo 'organizer: '^$"me
+ for(a in $att)
+ echo 'attendee: '^$a
+ }
+} >$tmp
+
+if(! ~ $#print 0){
+ cat $tmp
+ exit 0
+}
+cat $tmp >$d/new
+if(! ~ $status ''){
+ echo 'pim/invite: '^$"status >[1=2]
+ exit failed
+}
+w=`{date -f 'WWW DD MMM hh:mm' $"start}
+echo $"summary^': '^$"w^' on '^$"cal
+if(! ~ $#att 0)
+ echo 'invited: '^$"att
+exit 0
diff --git a/pim/rc/invites b/pim/rc/invites
new file mode 100755
index 0000000..de193ed
--- /dev/null
+++ b/pim/rc/invites
@@ -0,0 +1,87 @@
+#!/bin/rc
+# pim/invites -- what is waiting for your answer.
+#
+# pim/invites the next thirty days
+# pim/invites -n 7 just this week
+# pim/invites -a everything, answered or not
+#
+# Only upcoming events are looked at: an invitation you never answered
+# for a meeting last March is not a thing you need to see. Walking the
+# date tree rather than every event also keeps this to a few dozen reads
+# instead of a few thousand.
+rfork e
+
+mtpt=/mnt/pim
+days=30
+only=()
+all=()
+
+while(~ $1 -*){
+ switch($1){
+ case -m
+ mtpt=$2; shift
+ case -n
+ days=$2; shift
+ case -c
+ only=($only $2); shift
+ case -a
+ all=1
+ case *
+ echo 'usage: invites [-m mtpt] [-n days] [-c cal] [-a]' >[1=2]
+ exit usage
+ }
+ shift
+}
+
+cals=$only
+if(~ $#cals 0)
+ cals=`{pim/calendars -m $mtpt}
+
+seen=/tmp/invites.seen.$pid
+out=/tmp/invites.out.$pid
+fn sigexit { rm -f $seen $out }
+>$seen
+>$out
+
+now=`{date -n}
+i=0
+while(test $i -lt $days){
+ sec=`{echo $now + $i '*' 86400 | bc}
+ day=`{date -f YYYY/MM/DD $sec}
+ for(c in $cals){
+ d=$mtpt/calendars/$c/events/date/$day
+ if(test -d $d)
+ for(f in $d/*){
+ ev=`{sed -n 's/^event: //p' $f}
+ if(! ~ $#ev 0){
+ e=`{cleanname $d/$"ev}
+ # one line per event, not per occurrence
+ if(! grep -s '^'^$"e^'$' $seen){
+ echo $"e >>$seen
+ if(test -f $e/partstat){
+ st=`{cat $e/partstat}
+ show=()
+ if(~ $"st NEEDS-ACTION)
+ show=1
+ if(! ~ $#all 0)
+ show=1
+ if(! ~ $#show 0){
+ ep=`{sed -n 's/^epoch: //p' $f}
+ when=`{date -f 'WWW DD MMM hh:mm' $"ep}
+ sum=`{sed -n 's/^summary: //p' $f}
+ echo $"ep^' '^$"when^' '^$"st^' '^$"sum^' '^$c >>$out
+ }
+ }
+ }
+ }
+ }
+ }
+ i=`{echo $i + 1 | bc}
+}
+
+if(! test -s $out){
+ echo 'nothing waiting in the next '^$"days^' days'
+ exit 0
+}
+sort -n $out | awk -F' ' '{printf "%-22s %-13s %-9s %s\n", $2, $3, $5, $4}'
+exit 0
diff --git a/pim/rc/rsvp b/pim/rc/rsvp
new file mode 100755
index 0000000..0e21f04
--- /dev/null
+++ b/pim/rc/rsvp
@@ -0,0 +1,111 @@
+#!/bin/rc
+# pim/rsvp -- answer an invitation.
+#
+# pim/rsvp accepted 'meet with ben'
+# pim/rsvp declined /mnt/pim/calendars/work/events/date/2026/08/24/1000-API-WG
+#
+# Takes an occurrence path, as pim/agenda -p prints and cal9 plumbs, or a
+# pattern to search for. The answer goes to the event's partstat, and
+# what happens next is the backend's business: a caldav server with
+# calendar-auto-schedule updates your copy and mails the organiser, and
+# a published .ics refuses outright, which is the honest answer there.
+rfork e
+
+mtpt=/mnt/pim
+days=90
+only=()
+
+while(~ $1 -*){
+ switch($1){
+ case -m
+ mtpt=$2; shift
+ case -c
+ only=($only $2); shift
+ case -n
+ days=$2; shift
+ case *
+ echo 'usage: rsvp [-m mtpt] [-c cal] [-n days] accepted|declined|tentative path|pattern' >[1=2]
+ exit usage
+ }
+ shift
+}
+if(test $#* -lt 2){
+ echo 'usage: rsvp [-m mtpt] [-c cal] [-n days] accepted|declined|tentative path|pattern' >[1=2]
+ exit usage
+}
+
+want=`{echo $1 | tr a-z A-Z}
+shift
+switch($want){
+case ACCEPTED DECLINED TENTATIVE
+ ;
+case *
+ echo 'pim/rsvp: want accepted, declined or tentative' >[1=2]
+ exit usage
+}
+pat=$"*
+
+# an occurrence path names one outright; anything else is a search
+occ=()
+if(test -f $"pat)
+ occ=$"pat
+if(~ $#occ 0){
+ hits=`{pim/show -m $mtpt -n $days $only -a $"pat >[2]/dev/null | sed -n 's/^at *//p'}
+ if(~ $#hits 0){
+ echo 'pim/rsvp: nothing matching '^$"pat >[1=2]
+ exit notfound
+ }
+ # A recurring event matches once per occurrence, and the answer
+ # applies to the series, so count distinct events rather than hits.
+ evs=()
+ for(h in $hits){
+ hd=`{basename -d $h}
+ he=`{sed -n 's/^event: //p' $h}
+ if(! ~ $#he 0)
+ evs=($evs `{cleanname $hd/$"he})
+ }
+ evs=`{echo $evs | tr ' ' '\n' | sort -u}
+ if(! ~ $#evs 1){
+ echo 'pim/rsvp: '^$#evs^' events match; be more specific:' >[1=2]
+ for(x in $evs)
+ echo ' '^$x >[1=2]
+ exit ambiguous
+ }
+ occ=$hits(1)
+}
+
+# the occurrence points at its event; 9P has no symlinks, so it is a path
+d=`{basename -d $"occ}
+ev=`{sed -n 's/^event: //p' $"occ}
+if(~ $#ev 0){
+ echo 'pim/rsvp: '^$"occ^' has no event: line' >[1=2]
+ exit noevent
+}
+e=`{cleanname $d/$"ev}
+
+if(! test -f $e/partstat){
+ echo 'pim/rsvp: '^$"e^' has no partstat' >[1=2]
+ exit nopartstat
+}
+
+# Say why before the write fails. A published .ics has nowhere to put a
+# reply and no invitation to reply to, and "permission denied" does not
+# explain that.
+c=`{echo $"e | sed 's|(/.*/calendars/[^/]+)/.*|\1|'}
+caps=`{sed -n 's/^caps //p' $"c/ctl >[2]/dev/null}
+# ~ takes its first argument as the subject, so `~ $caps rsvp` asks
+# whether "read" is "rsvp". Walk the list instead.
+can=()
+for(x in $caps)
+ if(~ $x rsvp)
+ can=1
+if(! ~ $#caps 0)
+ if(~ $#can 0){
+ echo 'pim/rsvp: '^`{basename $"c}^' is '^$"caps^', it cannot answer invitations' >[1=2]
+ exit readonly
+ }
+
+echo $want >$e/partstat
+sum=`{sed -n 's/^summary: //p' $"occ}
+echo $"sum^': '^$want
+exit 0
diff --git a/pim/rc/show b/pim/rc/show
index da23973..b22ca82 100755
--- a/pim/rc/show
+++ b/pim/rc/show
@@ -9,6 +9,7 @@ mtpt=/mnt/pim
days=90
all=()
only=()
+start=()
while(~ $1 -*){
switch($1){
@@ -20,14 +21,21 @@ while(~ $1 -*){
all=1
case -c
only=($only $2); shift
+ case -d
+ start=`{seconds $2}
+ if(~ $#start 0){
+ echo 'pim/show: cannot read the date '^$2 >[1=2]
+ exit baddate
+ }
+ shift
case *
- echo 'usage: show [-m mtpt] [-n days] [-c cal] [-a] path|pattern' >[1=2]
+ echo 'usage: show [-m mtpt] [-n days] [-d date] [-c cal] [-a] path|pattern' >[1=2]
exit usage
}
shift
}
if(~ $#* 0){
- echo 'usage: show [-m mtpt] [-n days] [-c cal] [-a] path|pattern' >[1=2]
+ echo 'usage: show [-m mtpt] [-n days] [-d date] [-c cal] [-a] path|pattern' >[1=2]
exit usage
}
pat=$"*
@@ -76,7 +84,9 @@ fn search {
cals=$only
if(~ $#cals 0)
cals=`{pim/calendars -m $mtpt}
- now=`{date -n}
+ now=$start
+ if(~ $#now 0)
+ now=`{date -n}
i=0
while(test $i -lt $days){
sec=`{echo $now + $i '*' 86400 | bc}