@prefix sioc: <http://rdfs.org/sioc/ns#> .
@prefix dc: <http://purl.org/dc/elements/1.1/> .
@prefix dcterms: <http://purl.org/dc/terms/> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
@prefix content: <http://purl.org/rss/1.0/modules/content/> .
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .

<https://lhero.org/portfolio/lh-calendars/>
  a sioc:Post ;
  dc:title "Architecture" ;
  dcterms:identifier 153088 ;
  dc:modified "2026-11-01T16:41:28Z"^^xsd:dateTime ;
  dc:created "2026-11-01T16:41:28Z"^^xsd:dateTime ;
  sioc:link <https://lhero.org/portfolio/lh-calendars/architecture/> ;
  sioc:has_creator <https://lhero.org/author/1/#account> ;
  sioc:has_container <https://lhero.org/#posts> ;
  content:encoded """<ul class="lh_portfolio-meta"><li><strong>Part of:</strong> <a href="https://lhero.org/portfolio/lh-calendars/">LH Calendars</a></li></ul>
<p class="wp-block-paragraph">LH Calendars is a CalDAV provider for LH DAV, which owns the protocol, login (application passwords), sync tokens and XML. LH Calendars registers two providers on <code>lh_dav_collection_providers</code>; both serve <code>calendars/{user ID}/{collection}/</code> on the user&#8217;s home site only.</p>



<h3 class="wp-block-heading">Home site</h3>



<p class="wp-block-paragraph"><code>LH_Calendars::return_home_site_id()</code>: the main site for super admins, otherwise the user&#8217;s primary site if they still belong to it, else their first site. Serving only there means someone who adds accounts for several sites does not see every calendar twice. LH DAV&#8217;s setup profile leaves out CalDAV on other sites, and the profile screen there links to the home site.</p>



<h3 class="wp-block-heading">Personal calendars (<code>LH_Calendars_Personal_Provider</code>)</h3>



<ul class="wp-block-list">
<li>Writable, owned by the user. Implements <code>LH_DAV_Collection_Manager</code>, so devices make (MKCALENDAR or extended MKCOL), rename, recolour, reorder and delete them. Ids beginning <code>org-</code> are reserved; at most 50 per person.</li>



<li>A default calendar, <code>personal/</code>, is made the first time a user&#8217;s calendars are listed; network user meta <code>lh_calendars-default_calendar_made</code> stops a deleted default coming back.</li>



<li>Stored in two network tables: <code>{base_prefix}lh_calendars_calendars</code> (uri, name, description, colour, components, order) and <code>{base_prefix}lh_calendars_objects</code> (one row per entry: the iCalendar exactly as sent, with UID, component, and start and end in UTC copied out for queries). Keyed on the site option <code>lh_calendars-schema_version</code>.</li>



<li>An entry is one VCALENDAR with one event, to-do or journal entry and its recurrence exceptions. A calendar takes only the components it was made for; two entries may not share a UID (409).</li>



<li>Changes are logged in LH DAV&#8217;s change log against the main site (the collection&#8217;s <code>site_id</code>), since the data is network-wide. Deleting a user deletes their calendars.</li>
</ul>



<h3 class="wp-block-heading">Organisation calendars (<code>LH_Calendars_Organisations_Provider</code>)</h3>



<ul class="wp-block-list">
<li>One read-only collection, <code>org-{site ID}</code>, per site the user belongs to (<code>get_blogs_of_user()</code>), can read on, and where GatherPress is active.</li>



<li>Holds that site&#8217;s <code>gatherpress_event</code> posts starting from 90 days ago, in the statuses the user may see, decided on the event&#8217;s own site with <code>user_can()</code>: publish for everyone; private with the post type&#8217;s <code>read_private_posts</code>; LH Membership&#8217;s statuses with the capability <code>LH_Membership_access_control_class::return_view_capability_for_status()</code> names (<code>read_restricted_posts</code>, <code>read_confidential_posts</code>), or signed-in for <code>logged_in</code>.</li>



<li>Events are written as iCalendar on their own site (<code>LH_Calendars_Ical::build_event()</code>): UTC times, title, plain-text excerpt or content, venue name, address and GEO, online link, topics as CATEGORIES, permalink. No attendees or organiser email. UID <code>gatherpress-event-{ID}@{site host}</code>; resource <code>event-{ID}.ics</code>.</li>



<li><code>LH_Calendars_Event_Tracker</code> logs saves (<code>wp_after_insert_post</code>), trashing, deletion and date meta changes on the event&#8217;s site against <code>calendar/0/org-{site ID}</code>, so every member&#8217;s sync sees them wherever their home site is. Whether a member still sees a changed event is worked out at sync time.</li>



<li>The collection state carries the statuses the user may see and the month, so a change in access, or events leaving the window, restarts their sync. Venue edits reach phones when the event is next edited.</li>
</ul>



<h3 class="wp-block-heading">Profile and download</h3>



<p class="wp-block-paragraph"><code>LH_Calendars_Profile</code> adds &#8220;Your calendars&#8221; to the member&#8217;s own profile screen, and serves <code>GET /wp-json/lh-calendars/v1/export/{calendar}</code>: one .ics file of a personal calendar, timezones merged, for the signed-in user only.</p>



<p class="wp-block-paragraph">iCalendar is read and written with sabre/vobject, installed with Composer into <code>vendor/</code>.</p>
"""^^rdf:XMLLiteral ;
  sioc:content """Part of: LH Calendars
LH Calendars is a CalDAV provider for LH DAV, which owns the protocol, login (application passwords), sync tokens and XML. LH Calendars registers two providers on lh_dav_collection_providers; both serve calendars/{user ID}/{collection}/ on the user&#8217;s home site only.



Home site



LH_Calendars::return_home_site_id(): the main site for super admins, otherwise the user&#8217;s primary site if they still belong to it, else their first site. Serving only there means someone who adds accounts for several sites does not see every calendar twice. LH DAV&#8217;s setup profile leaves out CalDAV on other sites, and the profile screen there links to the home site.



Personal calendars (LH_Calendars_Personal_Provider)




Writable, owned by the user. Implements LH_DAV_Collection_Manager, so devices make (MKCALENDAR or extended MKCOL), rename, recolour, reorder and delete them. Ids beginning org- are reserved; at most 50 per person.



A default calendar, personal/, is made the first time a user&#8217;s calendars are listed; network user meta lh_calendars-default_calendar_made stops a deleted default coming back.



Stored in two network tables: {base_prefix}lh_calendars_calendars (uri, name, description, colour, components, order) and {base_prefix}lh_calendars_objects (one row per entry: the iCalendar exactly as sent, with UID, component, and start and end in UTC copied out for queries). Keyed on the site option lh_calendars-schema_version.



An entry is one VCALENDAR with one event, to-do or journal entry and its recurrence exceptions. A calendar takes only the components it was made for; two entries may not share a UID (409).



Changes are logged in LH DAV&#8217;s change log against the main site (the collection&#8217;s site_id), since the data is network-wide. Deleting a user deletes their calendars.




Organisation calendars (LH_Calendars_Organisations_Provider)




One read-only collection, org-{site ID}, per site the user belongs to (get_blogs_of_user()), can read on, and where GatherPress is active.



Holds that site&#8217;s gatherpress_event posts starting from 90 days ago, in the statuses the user may see, decided on the event&#8217;s own site with user_can(): publish for everyone; private with the post type&#8217;s read_private_posts; LH Membership&#8217;s statuses with the capability LH_Membership_access_control_class::return_view_capability_for_status() names (read_restricted_posts, read_confidential_posts), or signed-in for logged_in.



Events are written as iCalendar on their own site (LH_Calendars_Ical::build_event()): UTC times, title, plain-text excerpt or content, venue name, address and GEO, online link, topics as CATEGORIES, permalink. No attendees or organiser email. UID gatherpress-event-{ID}@{site host}; resource event-{ID}.ics.



LH_Calendars_Event_Tracker logs saves (wp_after_insert_post), trashing, deletion and date meta changes on the event&#8217;s site against calendar/0/org-{site ID}, so every member&#8217;s sync sees them wherever their home site is. Whether a member still sees a changed event is worked out at sync time.



The collection state carries the statuses the user may see and the month, so a change in access, or events leaving the window, restarts their sync. Venue edits reach phones when the event is next edited.




Profile and download



LH_Calendars_Profile adds &#8220;Your calendars&#8221; to the member&#8217;s own profile screen, and serves GET /wp-json/lh-calendars/v1/export/{calendar}: one .ics file of a personal calendar, timezones merged, for the signed-in user only.



iCalendar is read and written with sabre/vobject, installed with Composer into vendor/.
""" ;
  sioc:topic <https://lhero.org/?taxonomy=author&term=cap-1>, <https://lhero.org/?taxonomy=lh_object_key&term=lh-portfolio_doc_20261101t054100-lh-calendars-architecture> .

<https://lhero.org/author/1/#account> rdfs:seeAlso <https://lhero.org/author/1/?feed=lhrdf&format=turtle> .
<https://lhero.org/?taxonomy=author&term=cap-1> rdfs:seeAlso <https://lhero.org/?taxonomy=author&term=cap-1&feed=lhrdf&format=turtle> .
<https://lhero.org/?taxonomy=lh_object_key&term=lh-portfolio_doc_20261101t054100-lh-calendars-architecture> rdfs:seeAlso <https://lhero.org/?taxonomy=lh_object_key&term=lh-portfolio_doc_20261101t054100-lh-calendars-architecture&feed=lhrdf&format=turtle> .
