From 09cbd3054f559c3ff826f8b5d40579047429e3b5 Mon Sep 17 00:00:00 2001 From: Mia Bennett Date: Thu, 9 Apr 2026 11:33:13 +0930 Subject: [PATCH 1/8] test(events): [PPT-2247] filter public events --- drivers/place/public_events_spec.cr | 136 ++++++++++++++++++++++++++++ 1 file changed, 136 insertions(+) create mode 100644 drivers/place/public_events_spec.cr diff --git a/drivers/place/public_events_spec.cr b/drivers/place/public_events_spec.cr new file mode 100644 index 0000000000..ace3ddfc78 --- /dev/null +++ b/drivers/place/public_events_spec.cr @@ -0,0 +1,136 @@ +require "placeos-driver/spec" +require "place_calendar" + +DriverSpecs.mock_driver "Place::PublicEvents" do + system({ + Bookings: {BookingsMock}, + Calendar: {CalendarMock}, + }) + + # BookingsMock publishes its events in on_load, which triggers the + # Bookings_1 :bookings subscription in our driver. Give it a moment to fire. + sleep 200.milliseconds + + # ----------------------------------------------------------------------- + # Test 1: subscription populates the public events cache automatically + # ----------------------------------------------------------------------- + events = status[:public_events].as_a + events.size.should eq(1) + events[0]["id"].as_s.should eq("evt-public-1") + events[0]["title"].as_s.should eq("Public Conference") + + # ----------------------------------------------------------------------- + # Test 2: events without extended_properties.public are excluded + # ----------------------------------------------------------------------- + events.none? { |e| e["id"].as_s == "evt-private-no-ext" }.should be_true + + # ----------------------------------------------------------------------- + # Test 3: events with extended_properties.public = false are excluded + # ----------------------------------------------------------------------- + events.none? { |e| e["id"].as_s == "evt-private-explicit" }.should be_true + + # ----------------------------------------------------------------------- + # Test 4: only allowlisted fields are present in the public cache + # ----------------------------------------------------------------------- + events[0]["event_start"].as_i64.should be > 0_i64 + events[0]["event_end"].as_i64.should be > 0_i64 + events[0]["attendees"]?.should be_nil + events[0]["host"]?.should be_nil + events[0]["body"]?.should be_nil + events[0]["online_meeting_url"]?.should be_nil + events[0]["creator"]?.should be_nil + + # ----------------------------------------------------------------------- + # Test 5: update_public_events triggers a Bookings re-poll and returns nil; + # the cache is repopulated via the :bookings subscription binding. + # ----------------------------------------------------------------------- + exec(:update_public_events).get + sleep 200.milliseconds + updated_events = status[:public_events].as_a + updated_events.size.should eq(1) + updated_events[0]["id"].as_s.should eq("evt-public-1") + + # ----------------------------------------------------------------------- + # Test 6: register_attendee appends the guest via the Calendar driver + # ----------------------------------------------------------------------- + exec(:register_attendee, "evt-public-1", "Alice Smith", "alice@external.com").get.should be_true + + attendees = system(:Calendar)[:updated_attendees].as_a + attendees.any? { |a| a["email"].as_s == "alice@external.com" }.should be_true + attendees.any? { |a| a["name"].as_s == "Alice Smith" }.should be_true + + # ----------------------------------------------------------------------- + # Test 7: register_attendee returns false for unknown event IDs + # ----------------------------------------------------------------------- + exec(:register_attendee, "evt-private-no-ext", "Bob", "bob@example.com").get.should be_false + + # Calendar must not have been called again — updated_attendees unchanged + system(:Calendar)[:updated_attendees].as_a + .none? { |a| a["email"].as_s == "bob@example.com" } + .should be_true +end + +# :nodoc: +# Simulates the Bookings driver. Publishes a fixed set of three events on load +# so the PublicEvents driver's subscription fires immediately: +# - one explicitly public (should appear in the cache) +# - one with no properties (should be excluded) +# - one explicitly non-public (should be excluded) +class BookingsMock < DriverSpecs::MockDriver + def on_load + now = Time.utc + self[:bookings] = [ + PlaceCalendar::Event.new( + id: "evt-public-1", + host: "organizer@company.com", + title: "Public Conference", + event_start: now + 1.day, + event_end: now + 1.day + 2.hours, + extended_properties: Hash(String, String?){"public" => "true"}, + attendees: [PlaceCalendar::Event::Attendee.new(name: "Internal Person", email: "internal@company.com")], + ), + PlaceCalendar::Event.new( + id: "evt-private-no-ext", + host: "team@company.com", + title: "Internal Meeting", + event_start: now + 2.days, + event_end: now + 2.days + 1.hour, + ), + PlaceCalendar::Event.new( + id: "evt-private-explicit", + host: "exec@company.com", + title: "Executive Briefing", + event_start: now + 3.days, + event_end: now + 3.days + 1.hour, + extended_properties: Hash(String, String?){"public" => "false"}, + ), + ] + end + + def poll_events : Nil + # Re-publish current bookings to exercise the subscription path. + on_load + end +end + +# :nodoc: +# Simulates the Calendar driver (Microsoft::GraphAPI / Place::CalendarCommon). +# get_event returns a PlaceCalendar::Event directly. +# update_event records the final attendees list for assertion. +class CalendarMock < DriverSpecs::MockDriver + def get_event(calendar_id : String, event_id : String, user_id : String? = nil) : PlaceCalendar::Event + now = Time.utc + PlaceCalendar::Event.new( + id: event_id, + host: calendar_id, + title: "Public Conference", + event_start: now + 1.day, + event_end: now + 1.day + 2.hours, + ) + end + + def update_event(event : PlaceCalendar::Event, user_id : String? = nil, calendar_id : String? = nil) : PlaceCalendar::Event + self[:updated_attendees] = event.attendees + event + end +end From d3d5c317e2b2c50e60ef6b73336d83f6736915d2 Mon Sep 17 00:00:00 2001 From: Mia Bennett Date: Thu, 9 Apr 2026 11:33:36 +0930 Subject: [PATCH 2/8] feat(public_events): [PPT-2247] filter public events --- drivers/place/public_events.cr | 88 ++++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 drivers/place/public_events.cr diff --git a/drivers/place/public_events.cr b/drivers/place/public_events.cr new file mode 100644 index 0000000000..8abf544d98 --- /dev/null +++ b/drivers/place/public_events.cr @@ -0,0 +1,88 @@ +require "placeos-driver" +require "place_calendar" + +# Filters Bookings event cache down to public events for unauthenticated access. +# Uses the Calendar driver for guest registration. +class Place::PublicEvents < PlaceOS::Driver + descriptive_name "PlaceOS Public Events" + generic_name :PublicEvents + description %(Caches public events for external access and handles guest registration) + + accessor bookings : Bookings_1 + accessor calendar : Calendar_1 + + @all_bookings : Array(PlaceCalendar::Event) = [] of PlaceCalendar::Event + @public_event_ids : Set(String) = Set(String).new + + bind Bookings_1, :bookings, :on_bookings_change + + private def on_bookings_change(_subscription, new_value : String) + @all_bookings = Array(PlaceCalendar::Event).from_json(new_value) + filter_and_cache + rescue error + logger.warn(exception: error) { "failed to process bookings update" } + end + + private def filter_and_cache : Array(PlaceCalendar::Event) + public_events = @all_bookings.select do |event| + event.extended_properties.try { |props| props["public"]? == "true" } + end + @public_event_ids = public_events.compact_map(&.id).to_set + self["public_events"] = public_events.map { |e| PublicEvent.new(e) } + public_events + end + + # Forces a Bookings re-poll then re-applies the public filter. + @[Security(Level::Administrator)] + def update_public_events : Nil + bookings.poll_events.get + end + + # Appends an external attendee to the calendar event. + def register_attendee(event_id : String, name : String, email : String) : Bool + unless @public_event_ids.includes?(event_id) + logger.warn { "#{event_id} is not a known public event" } + return false + end + + cal_id = system.email.presence + unless cal_id + logger.error { "system has no calendar email configured" } + return false + end + + event_data = calendar.get_event(cal_id, event_id).get + unless event_data + logger.warn { "event #{event_id} not found in calendar" } + return false + end + + event = PlaceCalendar::Event.from_json(event_data.to_json) + event.attendees << PlaceCalendar::Event::Attendee.new(name: name, email: email) + calendar.update_event(event, calendar_id: cal_id).get + true + end + + # Fields that are safe to expose publicly. + private struct PublicEvent + include JSON::Serializable + + getter id : String? + getter title : String? + getter event_start : Int64 + getter event_end : Int64? + getter location : String? + getter timezone : String? + getter? all_day : Bool + + def initialize(event : PlaceCalendar::Event) + @id = event.id + @title = event.title + @event_start = event.event_start.to_unix + @event_end = event.event_end.try(&.to_unix) + @location = event.location + @timezone = event.timezone + @all_day = event.all_day? + end + end +end From da44620b25fc0bed8aa63090791099d8b5efbdbe Mon Sep 17 00:00:00 2001 From: Mia Bennett Date: Thu, 9 Apr 2026 11:33:56 +0930 Subject: [PATCH 3/8] docs(public_events): [PPT-2247] filter public events --- drivers/place/public_events_readme.md | 54 +++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 drivers/place/public_events_readme.md diff --git a/drivers/place/public_events_readme.md b/drivers/place/public_events_readme.md new file mode 100644 index 0000000000..1273d48355 --- /dev/null +++ b/drivers/place/public_events_readme.md @@ -0,0 +1,54 @@ +# Public Events Readme + +Docs on the PlaceOS Public Events driver. +This driver filters the Bookings event cache down to publicly visible events and handles guest registration, enabling unauthenticated access to selected calendar events. + +* Subscribes to the Bookings driver's `:bookings` status and filters events where `extended_properties["public"] == "true"` +* Caches the filtered set of public events (with a reduced set of safe fields) as the `:public_events` status +* Provides a `register_attendee` function for appending external (guest) attendees to a public event via the Calendar driver + + +## Requirements + +Requires the following drivers in the same system: + +* Bookings - for the room/calendar event cache and polling +* Calendar - for reading and updating calendar events when registering attendees + +The system must also have a calendar email configured (used as the `calendar_id` when calling the Calendar driver). + + +## How It Works + +1. The Bookings driver polls the calendar and publishes all events to its `:bookings` status +2. PublicEvents receives the update via the subscription binding and filters to events where `extended_properties["public"] == "true"` +3. The filtered events are stored in `:public_events` with only safe, non-sensitive fields exposed: `id`, `title`, `event_start`, `event_end`, `location`, `timezone`, `all_day` +4. When a guest registers, `register_attendee` checks the event is in the public set, fetches it from the Calendar driver, appends the attendee, and writes it back + + +## Public System Usage + +This driver is intended to be placed in the same system as the public events calendar. It follows the same public system access pattern as the WebRTC driver — a Guest JWT is issued to the caller after passing the invisible Google reCAPTCHA, granting read access to the `:public_events` status and the ability to call `register_attendee`. + + +## Functions + +### `register_attendee(event_id, name, email) : Bool` + +Appends an external attendee to a public calendar event. + +* Returns `true` on success +* Returns `false` if the `event_id` is not in the public events set, or if the system has no calendar email configured + +```yaml +# Example call +function: register_attendee +args: + event_id: "evt-abc-123" + name: "Alice Smith" + email: "alice@external.com" +``` + +### `update_public_events : Nil` + +Administrator-only. Triggers a Bookings re-poll and repopulates the public events cache via the subscription binding. \ No newline at end of file From 6a5e056a56535c2638f684d260876af4faa8bba9 Mon Sep 17 00:00:00 2001 From: Mia Bennett Date: Thu, 9 Apr 2026 11:35:45 +0930 Subject: [PATCH 4/8] test(ameba): allow not_nill in spec files --- .ameba.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.ameba.yml b/.ameba.yml index 5e2fb33645..29ed51c383 100644 --- a/.ameba.yml +++ b/.ameba.yml @@ -5,6 +5,10 @@ Lint/DebugCalls: Excluded: - drivers/**/*_spec.cr +Lint/NotNil: + Excluded: + - drivers/**/*_spec.cr + # NOTE: These should all be reviewed on an individual basis to see if their # complexity can be reasonably reduced. Metrics/CyclomaticComplexity: From abd51623f1008af35b2cc3dab0922b3c4315337f Mon Sep 17 00:00:00 2001 From: Mia Bennett Date: Thu, 9 Apr 2026 11:36:41 +0930 Subject: [PATCH 5/8] test(harness): makes the test harness work with git worktrees --- harness | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/harness b/harness index 8934361abc..6be5a1b505 100755 --- a/harness +++ b/harness @@ -4,6 +4,38 @@ # `u`: fail script if a variable is unset (unitialized) set -eu +# Holds the path to the saved .git worktree pointer while the harness runs. +_WORKTREE_BACKUP="" + +# Restore the original git worktree .git file if we replaced it. +# Safe to call even when no replacement was made. +restore_git_worktree() { + if [ -n "${_WORKTREE_BACKUP}" ] && [ -f "${_WORKTREE_BACKUP}" ]; then + rm -rf "${PWD}/.git" + mv "${_WORKTREE_BACKUP}" "${PWD}/.git" + _WORKTREE_BACKUP="" + fi +} + +# The test-harness needs a real git repo to read the current commit hash. +# When running inside a git worktree the .git entry is a file (not a directory) +# that points to an absolute path the container cannot see. Detect this and +# temporarily replace it with a self-contained repo for the duration of the run. +setup_git_for_harness() { + if [ -f "${PWD}/.git" ]; then + echo '░░░ Git worktree detected, creating temporary repo for harness...' + _WORKTREE_BACKUP="${PWD}/.git.worktree-bak" + cp "${PWD}/.git" "${_WORKTREE_BACKUP}" + rm "${PWD}/.git" + git -C "${PWD}" init -q + git -C "${PWD}" add -A + git -C "${PWD}" commit -q -m "temp: harness run" + fi +} + +# Always restore the worktree pointer on exit (covers errors and Ctrl-C). +trap restore_git_worktree EXIT + say_done() { printf "░░░ Done.\n" } @@ -56,6 +88,8 @@ format() { } report() { + setup_git_for_harness + echo '░░░ PlaceOS Driver Compilation Report' echo '░░░ Pulling images...' docker compose pull &> /dev/null From 99d140bd37fbfbc56447129a12d97b022eaa0c5c Mon Sep 17 00:00:00 2001 From: Mia Bennett Date: Thu, 9 Apr 2026 11:37:26 +0930 Subject: [PATCH 6/8] chore(shard.lock): update shards --- shard.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/shard.lock b/shard.lock index f566d1295a..fdd12ed44f 100644 --- a/shard.lock +++ b/shard.lock @@ -183,7 +183,7 @@ shards: neuroplastic: git: https://github.com/spider-gazelle/neuroplastic.git - version: 1.14.1 + version: 1.14.2 ntlm: git: https://github.com/spider-gazelle/ntlm.git @@ -251,7 +251,7 @@ shards: placeos-models: git: https://github.com/placeos/models.git - version: 9.86.0 + version: 9.86.2 pool: git: https://github.com/ysbaddaden/pool.git From 48a349e3f06d511ea4091e772a4fdf5b6b7eded9 Mon Sep 17 00:00:00 2001 From: Mia Bennett Date: Wed, 15 Apr 2026 15:45:16 +0930 Subject: [PATCH 7/8] fix(public_events): [PPT-2247] filter on the private field --- drivers/place/public_events.cr | 4 +--- drivers/place/public_events_spec.cr | 13 ++++++------- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/drivers/place/public_events.cr b/drivers/place/public_events.cr index 8abf544d98..c3a3d69dd0 100644 --- a/drivers/place/public_events.cr +++ b/drivers/place/public_events.cr @@ -24,9 +24,7 @@ class Place::PublicEvents < PlaceOS::Driver end private def filter_and_cache : Array(PlaceCalendar::Event) - public_events = @all_bookings.select do |event| - event.extended_properties.try { |props| props["public"]? == "true" } - end + public_events = @all_bookings.reject(&.private?) @public_event_ids = public_events.compact_map(&.id).to_set self["public_events"] = public_events.map { |e| PublicEvent.new(e) } public_events diff --git a/drivers/place/public_events_spec.cr b/drivers/place/public_events_spec.cr index ace3ddfc78..ef77e8a657 100644 --- a/drivers/place/public_events_spec.cr +++ b/drivers/place/public_events_spec.cr @@ -20,12 +20,12 @@ DriverSpecs.mock_driver "Place::PublicEvents" do events[0]["title"].as_s.should eq("Public Conference") # ----------------------------------------------------------------------- - # Test 2: events without extended_properties.public are excluded + # Test 2: private events are excluded # ----------------------------------------------------------------------- events.none? { |e| e["id"].as_s == "evt-private-no-ext" }.should be_true # ----------------------------------------------------------------------- - # Test 3: events with extended_properties.public = false are excluded + # Test 3: events explicitly marked private are also excluded # ----------------------------------------------------------------------- events.none? { |e| e["id"].as_s == "evt-private-explicit" }.should be_true @@ -73,9 +73,8 @@ end # :nodoc: # Simulates the Bookings driver. Publishes a fixed set of three events on load # so the PublicEvents driver's subscription fires immediately: -# - one explicitly public (should appear in the cache) -# - one with no properties (should be excluded) -# - one explicitly non-public (should be excluded) +# - one non-private event (should appear in the cache) +# - two private events (should be excluded) class BookingsMock < DriverSpecs::MockDriver def on_load now = Time.utc @@ -86,7 +85,6 @@ class BookingsMock < DriverSpecs::MockDriver title: "Public Conference", event_start: now + 1.day, event_end: now + 1.day + 2.hours, - extended_properties: Hash(String, String?){"public" => "true"}, attendees: [PlaceCalendar::Event::Attendee.new(name: "Internal Person", email: "internal@company.com")], ), PlaceCalendar::Event.new( @@ -95,6 +93,7 @@ class BookingsMock < DriverSpecs::MockDriver title: "Internal Meeting", event_start: now + 2.days, event_end: now + 2.days + 1.hour, + private: true, ), PlaceCalendar::Event.new( id: "evt-private-explicit", @@ -102,7 +101,7 @@ class BookingsMock < DriverSpecs::MockDriver title: "Executive Briefing", event_start: now + 3.days, event_end: now + 3.days + 1.hour, - extended_properties: Hash(String, String?){"public" => "false"}, + private: true, ), ] end From dd29e37a6cd13f0a9ab41bbe1030129bbf185f74 Mon Sep 17 00:00:00 2001 From: Mia Bennett Date: Wed, 15 Apr 2026 16:09:56 +0930 Subject: [PATCH 8/8] feat(public_events): [PPT-2247] add body field --- drivers/place/public_events.cr | 2 ++ drivers/place/public_events_readme.md | 6 +++--- drivers/place/public_events_spec.cr | 3 ++- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/drivers/place/public_events.cr b/drivers/place/public_events.cr index c3a3d69dd0..9ce64f89b3 100644 --- a/drivers/place/public_events.cr +++ b/drivers/place/public_events.cr @@ -67,6 +67,7 @@ class Place::PublicEvents < PlaceOS::Driver getter id : String? getter title : String? + getter body : String? getter event_start : Int64 getter event_end : Int64? getter location : String? @@ -76,6 +77,7 @@ class Place::PublicEvents < PlaceOS::Driver def initialize(event : PlaceCalendar::Event) @id = event.id @title = event.title + @body = event.body @event_start = event.event_start.to_unix @event_end = event.event_end.try(&.to_unix) @location = event.location diff --git a/drivers/place/public_events_readme.md b/drivers/place/public_events_readme.md index 1273d48355..f053d82b32 100644 --- a/drivers/place/public_events_readme.md +++ b/drivers/place/public_events_readme.md @@ -3,7 +3,7 @@ Docs on the PlaceOS Public Events driver. This driver filters the Bookings event cache down to publicly visible events and handles guest registration, enabling unauthenticated access to selected calendar events. -* Subscribes to the Bookings driver's `:bookings` status and filters events where `extended_properties["public"] == "true"` +* Subscribes to the Bookings driver's `:bookings` status and filters events where `private` is `false` * Caches the filtered set of public events (with a reduced set of safe fields) as the `:public_events` status * Provides a `register_attendee` function for appending external (guest) attendees to a public event via the Calendar driver @@ -21,8 +21,8 @@ The system must also have a calendar email configured (used as the `calendar_id` ## How It Works 1. The Bookings driver polls the calendar and publishes all events to its `:bookings` status -2. PublicEvents receives the update via the subscription binding and filters to events where `extended_properties["public"] == "true"` -3. The filtered events are stored in `:public_events` with only safe, non-sensitive fields exposed: `id`, `title`, `event_start`, `event_end`, `location`, `timezone`, `all_day` +2. PublicEvents receives the update via the subscription binding and filters to non-private events (`private == false`) +3. The filtered events are stored in `:public_events` with only safe, non-sensitive fields exposed: `id`, `title`, `body`, `event_start`, `event_end`, `location`, `timezone`, `all_day` 4. When a guest registers, `register_attendee` checks the event is in the public set, fetches it from the Calendar driver, appends the attendee, and writes it back diff --git a/drivers/place/public_events_spec.cr b/drivers/place/public_events_spec.cr index ef77e8a657..e1512df3a6 100644 --- a/drivers/place/public_events_spec.cr +++ b/drivers/place/public_events_spec.cr @@ -36,7 +36,7 @@ DriverSpecs.mock_driver "Place::PublicEvents" do events[0]["event_end"].as_i64.should be > 0_i64 events[0]["attendees"]?.should be_nil events[0]["host"]?.should be_nil - events[0]["body"]?.should be_nil + events[0]["body"]?.should_not be_nil events[0]["online_meeting_url"]?.should be_nil events[0]["creator"]?.should be_nil @@ -85,6 +85,7 @@ class BookingsMock < DriverSpecs::MockDriver title: "Public Conference", event_start: now + 1.day, event_end: now + 1.day + 2.hours, + body: "Join us for the annual public conference.", attendees: [PlaceCalendar::Event::Attendee.new(name: "Internal Person", email: "internal@company.com")], ), PlaceCalendar::Event.new(