From 09cbd3054f559c3ff826f8b5d40579047429e3b5 Mon Sep 17 00:00:00 2001 From: Mia Bennett Date: Thu, 9 Apr 2026 11:33:13 +0930 Subject: [PATCH 1/6] 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/6] 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/6] 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/6] 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/6] 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/6] 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