diff options
Diffstat (limited to 'pim/rc')
| -rwxr-xr-x | pim/rc/agenda | 15 | ||||
| -rwxr-xr-x | pim/rc/invite | 137 | ||||
| -rwxr-xr-x | pim/rc/invites | 87 | ||||
| -rwxr-xr-x | pim/rc/rsvp | 111 | ||||
| -rwxr-xr-x | pim/rc/show | 16 |
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} |
