How to display upcoming events in Nextcloud calendar using shell script

Display upcoming events in Nextcloud calendar using shell script as described earlier.

Shell script using curl and xmlstarlet as described in the previous blog post.

#!/bin/bash
# Display upcoming events in Nextcloud calendar
# Blog posts:
#   https://blog.sleeplessbeastie.eu/2018/06/11/how-to-display-upcoming-events-in-nextcloud-calendar-using-text-based-terminal-emulator/
#   https://blog.sleeplessbeastie.eu/2018/06/18/how-to-display-upcoming-events-in-nextcloud-calendar-using-shell-script/

# CalDav server and path
dav_server="https://cloud.example.org"
dav_path="/remote.php/dav/"

# Basic auth credentials
username="crono"
password="zabie"

# Get URL for the user's principal resource on the server
dav_user_path=$(curl --silent \
                     --request PROPFIND \
                     --header 'Content-Type: text/xml' \
                     --header 'Depth: 0' \
                     --data '<d:propfind xmlns:d="DAV:">
                               <d:prop>
                                 <d:current-user-principal />
                               </d:prop>
                             </d:propfind>' \
                     --user ${username}:${password} \
                     ${dav_server}${dav_path} | \
                xmlstarlet sel -t -v 'd:multistatus/d:response/d:propstat/d:prop/d:current-user-principal/d:href' -n) 

# Get URL that contains calendar collections owned by the user
dav_user_calendar_home_path=$(curl --silent \
                              --request PROPFIND \
                              --header 'Content-Type: text/xml' \
                              --header 'Depth: 0' \
                              --data '<d:propfind xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:caldav">
                                        <d:prop>
                                          <c:calendar-home-set />
                                        </d:prop>
                                      </d:propfind>' \
                              --user ${username}:${password} \
                              ${dav_server}${dav_user_path} | \
                         xmlstarlet sel -t -v 'd:multistatus/d:response/d:propstat/d:prop/cal:calendar-home-set/d:href' -n) 

# Get calendar paths                                      
dav_user_calendar_paths=$(curl --silent \
            	     --request PROPFIND \
                     --header 'Content-Type: text/xml' \
                     --header 'Depth: 1' \
                     --data '<d:propfind xmlns:d="DAV:" xmlns:cs="http://calendarserver.org/ns/"><d:prop><d:displayname/></d:prop></d:propfind>' \
                     --user ${username}:${password} \
                     ${dav_server}${dav_user_calendar_home_path} | \
		     xmlstarlet sel -t -m 'd:multistatus/d:response' -i  "string-length(d:propstat/d:prop/d:displayname)" -i "d:propstat/d:status='HTTP/1.1 200 OK'" -v "d:href" -n)

# Define start/end date
date_start=$(date  +"%Y%m%dT000000" -d today)
date_end=$(date  +"%Y%m%dT000000" -d tomorrow)

# Get data for each calendar
for dav_user_calendar_path in ${dav_user_calendar_paths}; do
  # calendar name
  calendar_name=$(curl --silent \
                       --request PROPFIND \
                       --header "Content-Type: text/xml" \
                       --header 'Depth: 0' \
                       --user ${username}:${password} \
                       ${dav_server}${dav_user_calendar_path} | \
                       xmlstarlet sel -t -v 'd:multistatus/d:response/d:propstat/d:prop/d:displayname' -n)
  # event types stored in this calendar e.g. VTODO, VEVENTS
  component_types=$(curl --silent \
                         --request PROPFIND \
                         --header "Content-Type: text/xml" \
                         --header 'Depth: 0' \
                         --user ${username}:${password} \
                         ${dav_server}${dav_user_calendar_path} | \
                    xmlstarlet sel -t -v 'd:multistatus/d:response/d:propstat/d:prop/cal:supported-calendar-component-set/cal:comp/@name' -n)

  # initial values
  events=""
  events_count="0"
  todos=""
  todos_count="0"
  overdue_todos=""
  overdue_todos_count="0"
  general_todos=""
  general_todos_count="0"
  inotrfc_todos=""
  inotrfc_todos_count="0"

  case "${component_types}" in
    *VEVENT*)
      # Today's events
      events=$(curl --silent \
                    --request REPORT \
                    --header "Depth: 1" \
                    --header "Content-Type: text/xml" \
                    --data '<c:calendar-query xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:caldav">
                              <d:prop><d:getetag /><c:calendar-data /></d:prop>
                              <c:filter>
                                <c:comp-filter name="VCALENDAR">
                                  <c:comp-filter name="VEVENT">
                                     <c:time-range  start="'${date_start}'" end="'${date_end}'"/>
                                  </c:comp-filter>
                                </c:comp-filter>
                              </c:filter>
                            </c:calendar-query>' \
                    --basic \
                    --user ${username}:${password} \
                    ${dav_server}${dav_user_calendar_path} | \
		    xmlstarlet sel -t -v 'd:multistatus/d:response/d:propstat/d:prop/cal:calendar-data' -n | \
               grep ^SUMMARY)
      events_count=$(echo "$events" | grep -c ^SUMMARY)
      ;&
    *VTODO*)
      # Tasks to be done today
      todos=$(curl --silent \
                    --request REPORT \
                    --header "Depth: 1" \
                    --header "Content-Type: text/xml" \
                    --data '<c:calendar-query xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:caldav">
                              <d:prop><d:getetag /><c:calendar-data /></d:prop>
                              <c:filter>
                                <c:comp-filter name="VCALENDAR">
                                  <c:comp-filter name="VTODO">
                                     <c:prop-filter name="DUE">
                                       <c:time-range start="'${date_start}'" end="'${date_end}'"/>
                                       <c:is-defined/>
                                     </c:prop-filter>
                                     <c:prop-filter name="COMPLETED">
                                       <c:is-not-defined/>
                                     </c:prop-filter>
                                  </c:comp-filter>
                                </c:comp-filter>
                              </c:filter>
                            </c:calendar-query>' \
                    --user ${username}:${password} \
                    ${dav_server}${dav_user_calendar_path} | \
               xmlstarlet sel -t -v 'd:multistatus/d:response/d:propstat/d:prop/cal:calendar-data' -n | \
                            
               grep ^SUMMARY)
      todos_count=$(echo "$todos" | grep -c ^SUMMARY)

      # Overdue tasks
      overdue_todos=$(curl --silent \
                    --request REPORT \
                    --header "Depth: 1" \
                    --header "Content-Type: text/xml" \
                    --data '<c:calendar-query xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:caldav">
                              <d:prop><d:getetag /><c:calendar-data /></d:prop>
                              <c:filter>
                                <c:comp-filter name="VCALENDAR">
                                  <c:comp-filter name="VTODO">
                                     <c:prop-filter name="DUE">
                                     <c:time-range end="'${date_start}'"/>
                                       <c:is-defined/>
                                     </c:prop-filter>
                                     <c:prop-filter name="COMPLETED">
                                       <c:is-not-defined/>
                                     </c:prop-filter>
                                  </c:comp-filter>
                                </c:comp-filter>
                              </c:filter>
                            </c:calendar-query>' \
                    --user ${username}:${password} \
                    ${dav_server}${dav_user_calendar_path} | \
               xmlstarlet sel -t -v 'd:multistatus/d:response/d:propstat/d:prop/cal:calendar-data' -n | \
               grep ^SUMMARY)
      overdue_todos_count=$(echo "$overdue_todos" | grep -c ^SUMMARY)

      # Tasks that belong to "Idea" category but does not contain RFC string
      inotrfc_todos=$(curl --silent \
                    --request REPORT \
                    --header "Depth: 1" \
                    --header "Content-Type: text/xml" \
                    --data '<c:calendar-query xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:caldav">
                              <d:prop><d:getetag /><c:calendar-data /></d:prop>
                              <c:filter>
                                <c:comp-filter name="VCALENDAR">
                                  <c:comp-filter name="VTODO">
                                    <c:prop-filter name="SUMMARY">
                                      <c:text-match negate-condition="yes" collation="i;ascii-casemap">rfc</c:text-match>
                                    </c:prop-filter>
                                    <c:prop-filter name="CATEGORIES">
                                      <c:text-match collation="i;ascii-casemap">idea</c:text-match>
                                    </c:prop-filter>
                                    <c:prop-filter name="COMPLETED">                                    
                                      <c:is-not-defined/>                                               
                                    </c:prop-filter> 
                                  </c:comp-filter>
                                </c:comp-filter>
                              </c:filter>                              
                            </c:calendar-query>' \
                    --user ${username}:${password} \
                    ${dav_server}${dav_user_calendar_path} | \
               xmlstarlet sel -t -v 'd:multistatus/d:response/d:propstat/d:prop/cal:calendar-data' -n | \
               grep ^SUMMARY)
      inotrfc_todos_count=$(echo "$inotrfc_todos" | grep -c ^SUMMARY)

      # Tasks without defined due date
      general_todos=$(curl --silent \
                    --request REPORT \
                    --header "Depth: 1" \
                    --header "Content-Type: text/xml" \
                    --data '<c:calendar-query xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:caldav">
                              <d:prop><d:getetag /><c:calendar-data /></d:prop>
                              <c:filter>
                                <c:comp-filter name="VCALENDAR">
                                  <c:comp-filter name="VTODO">
                                     <c:prop-filter name="DUE">
                                       <c:is-not-defined/>
                                     </c:prop-filter>
                                     <c:prop-filter name="COMPLETED">
                                       <c:is-not-defined/>
                                     </c:prop-filter>
                                  </c:comp-filter>
                                </c:comp-filter>
                              </c:filter>
                            </c:calendar-query>' \
                    --user ${username}:${password} \
                    ${dav_server}${dav_user_calendar_path} | \
               xmlstarlet sel -t -v 'd:multistatus/d:response/d:propstat/d:prop/cal:calendar-data' -n | \
               grep ^SUMMARY)
      general_todos_count=$(echo "$general_todos" | grep -c ^SUMMARY)
      ;&
  esac

  if [ "$events_count"        -ge "1" ] || \
     [ "$todos_count"         -ge "1" ] || \
     [ "$overdue_todos_count" -ge "1" ] || \
     [ "$general_todos_count" -ge "1" ] || \
     [ "$inotrfc_todos_count" -ge "1" ]; then
    echo $calendar_name 
  
    if [ "$events_count" -ge "1" ]; then
      echo "  Today's events: "
      printf "$events\n" | tr -s "\n" | sed "s/^SUMMARY://" | while read event; do
        echo "    * $event"
      done  
    else
      echo "  There are no events for today"
    fi

    if [ "$todos_count" -ge "1" ]; then
      echo "  Tasks to be done today"
      printf "$todos\n" | tr -s "\n" | sed "s/^SUMMARY://" | while read todo; do
        echo "    * $todo"
      done  
    else
      echo "  There are no tasks to be done today"
    fi

    if [ "$overdue_todos_count" -ge "1" ]; then
      echo "  Overdue tasks"
      printf "$overdue_todos\n" | tr -s "\n" | sed "s/^SUMMARY://" | while read todo; do
        echo "    * $todo"
      done  
    fi

    if [ "$inotrfc_todos_count" -ge "1" ]; then
      echo "  Tasks that belong to the \"Idea\" category but does not contain RFC string"
      printf "$inotrfc_todos\n" | tr -s "\n" | sed "s/^SUMMARY://" | while read todo; do
        echo "    * $todo"
      done  
    else
      echo "  There are no tasks that belong to the \"Idea\" category but does not contain RFC string"
    fi

    if [ "$general_todos_count" -ge "1" ]; then
      echo "  Tasks without defined due date"
      printf "$general_todos\n" | tr -s "\n" | sed "s/^SUMMARY://" | while read todo; do
        echo "    * $todo"
      done  
    else
      echo "  There are no tasks without defined due date"
    fi
  fi
done

Shell script output.

Personal
  Today's events: 
    * Configure VPN for Netflix
    * Renew car insurance
  There are no tasks to be done today
  There are no tasks that belong to the "Idea" category but does not contain RFC string
  Tasks without defined due date
    * Buy Radiant Historia for DS
Debian
  There are no events for today
  There are no tasks to be done today
  There are no tasks that belong to the "Idea" category but does not contain RFC string
  Tasks without defined due date
    * Install OpenVPN
    * Upgrade iptables firewall
Work
  There are no events for today
  There are no tasks to be done today
  There are no tasks that belong to the "Idea" category but does not contain RFC string
  Tasks without defined due date
    * Reset password for Safari Books Online
Projects
  Today's events: 
    * Install and configure Dokuwiki
  Tasks to be done today
    * Create blog post about CalDAV
  Overdue tasks
    * Inspect RFC4791
  Tasks that belong to the "Idea" category but does not contain RFC string
    * Create blog post about CalDAV
  There are no tasks without defined due date

This shell script and the prevoius blog post should be enough to create useful solution that filter calendar events and display these or send further using API to Slack or other service.

About Milosz Galazka

Milosz is a Linux Foundation Certified Engineer working for a successful Polish company as a system administrator and a long time supporter of Free Software Foundation and Debian operating system. He is also open for new opportunities and challenges.