Working with calendars in Community Solid Server
Calendars are probably one of things that we cannot do without,
whether it is for scheduling our work meetings,
planning our holidays, or
keeping track of all our weekend activities.
But scheduling a meeting with someone else
can be quite the hassle.
You can send an email or message,
but that does not work with multiple people.
You can create a Doodle, that half of the people fill in too late or not at all.
Or you can check their shared work calendar and send an invitation for a meeting on a free moment.
Only to get a decline in return, because they have a personal appointment that is not in the shared calendar.
In this blog post
we describe how you can avoid wasting your time like that,
by showing how you can define different types of calendars based on your existing calendars
and share them with others.
For this, we use the Community Solid Server and
our Solid Calendar Store.
Note that this is not a tutorial:
we do not discuss every detail of the server and the store.
There is an accompanying presentation in
PPTX and
PDF.
Running example
Consider the following example.
You maintain these two calendars:
work
contains all your work meetings.
personal
contains all events of your personal life,
such as sports activities, outings with loved ones, city trips, and so on.
Imagine that one of your colleagues wants to schedule a meeting with you.
They have direct access to all details of your work calendar,
so they can see when you are free.
Based on this calendar, your colleague sends you an invitation for
a meeting coming Friday at 15:00.
Although, you do not have any meetings planned at that moment,
you are not available for a meeting with them
because you are traveling with your family to Brussels for the weekend.
This information is not available in your work
calendar,
but it is available in your personal
calendar.
An easy solution would be to just share this calendar as well with your colleagues.
But, you might want to keep certain information in this calendar private,
such as medical appointments.
Another solution is the creation of a new calendar based on personal
that only shares the start and end dates of your events.
Each event has as title “Busy”.
We call this calendar personal (external)
, because
this calendar can be shared with external people.
You share this calendar with your colleagues.
They have to check now two calendars, work
and personal (external)
,
to find a time slot for a meeting.
This does not scale if you share more calendars with them.
And it gets worse when a meeting has to be scheduled
with multiple people with each person having multiple calendars.
A better solution is the creation of an aggregated calendar called busy
based on work
and personal (external)
.
This calendar contains all moments that you are busy.
Work meetings have all details,
but personal events have as title “Busy”.
You share this single calendar with your colleagues.
Now, they have a single calendar that they can use for scheduling a meeting with you.
The number of total calendars to check scales with the number of people that are participating in the meeting.
In the above image
you find an overview of how the different calendars are related to each other.
We remove the details of the personal
calendar through a transformation.
This new calendar is aggregated with the work
calendar,
resulting in a new calendar with all busy events.
Using custom stores to add functionality
One of the Community Solid Server’s main features is its extensibility:
it allows us to support new types of data and functionality,
through the use of custom stores.
In our case,
we want to support
- reading ICS files, commonly used for representing calendars, and
- manipulating these calendars.
Stores represent collections of resources and
allow manipulating these resources.
In our case, these resources are calendars and
in the Solid Calendar Store
we defined different stores that allow us to manipulate calendars.
In the above image
you find an overview of these stores and how they are related to each other.
We discuss the details in the following sections.
Transforming calendars
In our example we need to transform the personal
calendar:
only keep the start and end date of the events and
replace their titles with “Busy”.
In the code snippets below we explain the different parts of the configuration file
that we provide to the server.
For more details
we refer to the documentation of our store
and for a working configuration of the server
to the full example.
{
"@id": "my-calendar:PersonalGet",
"@type": "HttpGetStore",
"HttpGetStore:_options_url": "https://calendar.google.com/calendar/ical/ihqfct3hddiiskgvujr3df2pe0%40group.calendar.google.com/public/basic.ics"
}
In the above snippet,
we use an instance of an HTTP GET Store,
which is defined in Solid Calendar Store.
This store reads the ICS file of our calendar from a URL
via an HTTP GET request.
We identify this specific instance by my-calendar:PersonalGet
.
{
"@id": "my-calendar:PersonalCalendar",
"@type": "RepresentationConvertingStore",
"RepresentationConvertingStore:_source": {
"@id": "my-calendar:PersonalGet"
},
"RepresentationConvertingStore:_options_outConverter": {
"@id": "solid-calendar-store:RepresentationConverter"
}
}
We use this HTTP GET Store in the snippet above as a source
for an instance of a Representation Converting Store,
which is defined in the Community Solid Server.
This store enables converting our calendar data to different data representations,
through the use of a Representation Converter,
which is defined in Solid Calendar Store.
We provide it to the Representation Converting Store via the parameter _options_outConverter
.
This allows us to have a JSON representation of the calendar data,
which is originally in ICS.
This JSON representation is used by other stores.
This specific instance of the Representation Converting Store is identified by my-calendar:PersonalCalendar
.
{
"@id": "my-calendar:PersonalExternalCalendar",
"@type": "TransformationStore",
"TransformationStore:_options_rules": ["busy"],
"TransformationStore:_source": {
"@id": "my-calendar:PersonalCalendar"
},
"TransformationStore:_options_settingsPaths": [
"personal-external.yaml"
]
}
We use the Representation Converting Store in turn as a source for our personal (external)
calendar,
which is an instance of the Transformation Store.
This store is defined by Solid Calendar Store and
only works with the JSON representation of a calendar.
It allows us to define which transformations to execute via the parameters
settingsPaths
and rules
.
The former points to files that contain a list of one or more transformations.
The latter selects which transformation should be executed in this store.
In this case the busy
transformation looks like this:
transformation:
busy:
- match: '^.*$'
replace: "Busy"
removeFields: [description]
In practice this means that every title of an event that matches ^.*$
,
which is all titles, is marked as busy
by replacing the name of the event with “Busy”.
Additionally, the store removes the description
field from the event,
because we do no want to share this with others.
Aggregating calendars
In our example we need to aggregate the personal (external)
and work
calendar.
In the code snippets below we again explain the different, relevant parts of the configuration file.
{
"@id": "my-calendar:WorkCalendar",
"@type": "RepresentationConvertingStore",
"RepresentationConvertingStore:_source": {
"@id": "my-calendar:WorkGet"
},
"RepresentationConvertingStore:_options_outConverter": {
"@id": "solid-calendar-store:RepresentationConverter"
}
},
{
"@id": "my-calendar:WorkGet",
"@type": "HttpGetStore",
"HttpGetStore:_options_url": "https://calendar.google.com/calendar/ical/ihqfct3hddiiskgvujr3df2pe0%40group.calendar.google.com/public/basic.ics"
}
In the snippet above,
we define an HTTP GET Store and a Representation Converting Store for the work
calendar.
This is identical to the personal
calendar with exception to the URL serving the ICS file.
{
"@id": "my-calendar:BusyCalendar",
"@type": "AggregateStore",
"AggregateStore:_source1": {
"@id": "my-calendar:PersonalExternalCalendar"
},
"AggregateStore:_source2": {
"@id": "my-calendar:WorkCalendar"
}
}
We use an instance of an Aggregate Store,
which is defined in Solid Calendar Store,
that aggregates two calendars.
This specific instance is identified by my-calendar:BusyCalendar
and
we use the two previously defined calendars:
my-calendar:PersonalExternalCalendar
and my-calendar:WorkCalendar
.
{
"RegexRouterRule:_storeMap_key": "^/busy$",
"RegexRouterRule:_storeMap_value": {
"@id": "my-calendar:BusyCalendar"
}
}
Finally, we define which route is connected to our Aggregate Store.
In our case we want to return our busy events when requesting the path /busy
from our server.
Serving RDF representation of calendars
When we do a GET request to our newly created calendar with the current configuration,
we get the result as JSON.
For example, curl http://localhost:3000/busy
returns
{
"name":"Aggregated calendar of Personal example and Work example",
"events":[
{
"title":"[Personal example] Busy",
"startDate":"2021-04-08T15:00:00.000Z",
"endDate":"2021-04-08T17:00:00.000Z",
"hash":"c585549052f444427a1528a430c001b5",
"location":""
},
// The remainder of the events are excluded for brevity.
]
}
When Semantic Web applications want to use this calendar,
they prefer the RDF representation of this calendar instead of the JSON.
We achieve this by adding the following snippet.
{
"@id": "urn:solid-server:default:ChainedConverter",
"ChainedConverter:_converters": [{ "@id": "solid-calendar-store:json-to-rdf-converter" }]
}
Solid Calendar Store defines a converter,
identified by solid-calendar-store:json-to-rdf-converter
,
that generates RDF from a JSON representation of a calendar.
In the background this converter uses RML.io technologies for that,
through the Solid RML Store.
We add this converter to the Chained Converter of the server,
which determines which converter needs to be used based on the request of a client.
In our example we can do now curl -H 'accept: text/turtle' http://localhost:3000/busy
,
which returns
@prefix schema: <http://schema.org/> .
@prefix calendar: <http://example.com/calendar/> .
@prefix event: <http://example.com/event/> .
calendar:Aggregated%20calendar%20of%20Personal%20Time%20example%20and%20Work%20example
schema:event event:09ec6f847b05ebc845da09f9777bca98,
event:500fa723b22615892e91b64405b5a32c, event:98dd5445ee418eb6a0aeeb1da537a9a4,
event:b22e5eed1e7599d6ea64d9e1b1150bfb, event:c585549052f444427a1528a430c001b5;
schema:name "Aggregated calendar of Personal example and Work example" .
event:09ec6f847b05ebc845da09f9777bca98 a schema:Event;
schema:description "";
schema:endDate "2021-08-03T18:00:00.000Z";
schema:location "";
schema:name "[Work example] Meeting";
schema:startDate "2021-08-03T16:15:00.000Z" .
# The remainder of the events are excluded for brevity.
Serving ICS representation of calendars
Beside Semantic Web applications,
you might want to include your new calendars
directly in your existing calendar applications,
such as Google Calendar.
We achieve this by supporting ICS output for our calendars.
Similar to the supporting an RDF representation,
we add a JSON to ICS converter:
{
"@id": "urn:solid-server:default:ChainedConverter",
"ChainedConverter:_converters": [{ "@id": "JsonToIcsConverter" }]
}
In our example we can do now curl -H 'accept: text/calendar' http://localhost:3000/busy
,
which returns
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//sebbo.net//ical-generator//EN
NAME:Aggregated calendar of Free Time example and Work example
X-WR-CALNAME:Aggregated calendar of Free Time example and Work example
BEGIN:VEVENT
UID:7abb9807-4d85-47db-81da-10ccca0a7672
SEQUENCE:0
DTSTAMP:20210817T122516Z
DTSTART:20210408T150000Z
DTEND:20210408T170000Z
SUMMARY:[Free Time example] Busy
END:VEVENT
BEGIN:VEVENT
UID:dd379c3c-b653-4a49-91e0-78b00cdabe23
SEQUENCE:0
DTSTAMP:20210817T122516Z
DTSTART:20210330T060000Z
DTEND:20210330T160000Z
# The remainder of the events are excluded for brevity.
Wrapping up
We hope you now understand how you can create your own calendars by using
the Community Solid Server and
Solid Calendar Store.
You can find more information about both of them on their respective Github repositories
and by checking out the full example.
If you have questions or remarks, don’t hesitate to contact us!