Calendar notifications with a bot

After setting up Radicale as my self-hosted calendar system (CalDav), I hit the issue that my wife and I have a shared calendar and I couldn't find a good way to get notifications that a new event has been added/changed there, which was a nice feature of the iCloud calendar, which is what we were using before.

At first I set up a python script that would monitor the calendar every 30s and send us an e-mail if there's anything new, that worked fine but it wasn't very elegant: I'm already flooded with e-mails and this wouldn't make it any better. Also, if we wanted to talk about the event it would require forwarding the e-mail and have the discussion on another medium, while we've been mostly using Matrix and Riot to chat already.

So I decided to try out the matrix-python-sdk library and build a bot that would send a message to our chat room when there's something new on our calendar.

Making a basic bot

First, let's create a Python 3 virtual environment and install the packages we're going to use:

$ virtualenv --python=python3 venv
$ source venv/bin/activate
$ pip install matrix_client caldav icalendar

From there, let's assume you have a matrix server running on with an user called already joined a room with internal ID of !

from matrix_client.client import MatrixClient
client = MatrixClient("")
token = client.login_with_password(username="my-user", password="my-password")
room = client.get_rooms()["!"]
room.send_text("Hello World!")

And that's it! Can't get simpler than that!


Now, let's connect to the calendar:

import caldav
from icalendar import Calendar, Event
client = caldav.DAVClient(caldav_server,

From there we can list the events in the calendar:

principal = self.client.principal()
for calendar in principal.calendars():
    for event in
        asciidata ="ascii","ignore")
        c = Calendar.from_ical(asciidata)
        for comp in c.walk():
            if == "VEVENT":
                v = [d.strftime("%Y-%m-%d %H:%M:%S") \
                     for d in (comp[u"DTSTART"].dt,  \
                               comp[u"DTEND"].dt,    \

The idea is to just gather that list every 30s and see if there's anything new. The full source is on the bottom of this post.

Systemd service

Now, I want to run that script on start-up time on my server in the background, so let's create a systemd unit file called "caldav_bot.service":


ExecStart=/home/myuser/caldav_bot/venv/bin/python /home/myuser/caldav_bot/


And let's create a link for that in /etc/systemd/system:

$ sudo su
# cd /etc/systemd/system
# ln -s /home/myuser/caldav_bot/caldav_bot.service .
# systemctl start caldav_bot
# systemctl status caldav_bot

And the status message should say the daemon is running!

Full source for Calbot

Edit: Thanks to dubtooth on reddit for pointing out that the script needs a token for your matrix user, you can get one by doing:

curl -XPOST -d '{"type":"m.login.password", "user":"my-user", "password":"my-password"}' ""

#!/usr/bin/env python
from matrix_client.client import MatrixClient
import caldav
from icalendar import Calendar, Event
import pickle
import time
import os

config = {
    "caldav" : {
        "url"      : "",
        "user"     : "",
        "password" : ""
    "matrix" : {
        "user_id" : "",
        "room" : "",
        "base_url" : "",
        "token" : ""

class CaldavWatcher(object):
    def __init__(self, caldav_server, caldav_user, caldav_passwd):
        self.client = caldav.DAVClient(caldav_server, username=caldav_user, password=caldav_passwd)
        self.data_loc = "/tmp/caldav_data.set"
        open(self.data_loc, 'a+').close()
        os.chmod(self.data_loc, 700) # accessible only by root

    def sync(self):
            with open(self.data_loc, 'rb') as f:
                old_s = pickle.load(f)
            old_s = set()

        store = set()

        principal = self.client.principal()
        for calendar in principal.calendars():
            for event in
                asciidata ="ascii","ignore")
                c = Calendar.from_ical(asciidata)
                for comp in c.walk():
                    if == "VEVENT":
                        v = [d.strftime("%Y-%m-%d %H:%M:%S") for d in (comp[u"DTSTART"].dt, comp[u"DTEND"].dt, comp[u"DTSTAMP"].dt)]
                        store.add( (comp["SUMMARY"], v[0], v[1], v[2]) )

        with open(self.data_loc, 'wb') as f:
            pickle.dump(store, f)

        print("finished caldav sync")
        k = store - old_s
        return k

    def run(self):
        d = self.sync()
        if len(d) > 0:
            print("new events: %d" % len(d))
            msg = ""
            for k in d:
                summary = k[0]
                start = k[1]
                end   = k[2]
                msg += "New event: %s\n\tStart:\t%s\n\tEnd:\t%s\n" % (summary,start,end)
            if len(d) > 5: return "There is %d new events" % len(d)
            else: return msg
            return None

def main(config):

    matrix_config = config["matrix"]
    caldav_config = config["caldav"]

    # setup api/endpoint
    client = MatrixClient(matrix_config["base_url"],

    room = client.get_rooms()[matrix_config["room"]]

    cal = CaldavWatcher(caldav_config["url"],

    while True:
        m =
        print("message", m)
        if m:
            room.send_notice("New Calendar Events:\n" + m)


if __name__ == '__main__':

Edit: discussion on reddit.


I'm a cat-shadow floating in the web.