From cd522f7b13c12fe0fd3e8e9d22733749b6dd5cbd Mon Sep 17 00:00:00 2001 From: Mia Bennett Date: Wed, 17 Jul 2024 08:14:35 +0930 Subject: [PATCH] feat(events): #add_attendee (#326) --- spec/controllers/events_spec.cr | 282 +++++++++++++++++++++++ spec/controllers/helpers/event_helper.cr | 3 +- spec/spec_helper.cr | 4 +- src/controllers/events.cr | 192 ++++++++++++++- src/models/event.cr | 3 + 5 files changed, 481 insertions(+), 3 deletions(-) diff --git a/spec/controllers/events_spec.cr b/spec/controllers/events_spec.cr index 055f296c..7c05f23a 100644 --- a/spec/controllers/events_spec.cr +++ b/spec/controllers/events_spec.cr @@ -255,6 +255,288 @@ describe Events do end end + describe "permission", tags: ["auth", "group-event"] do + it "#add_attendee should NOT allow adding public or same tenant users to PRIVATE events" do + WebMock.stub(:post, "https://graph.microsoft.com/v1.0/users/dev%40acaprojects.onmicrosoft.com/calendar/events") + .to_return(body: File.read("./spec/fixtures/events/o365/create.json")) + + WebMock.stub(:get, "https://graph.microsoft.com/v1.0/users/room1%40example.com/calendar/events/AAMkADE3YmQxMGQ2LTRmZDgtNDljYy1hNDg1LWM0NzFmMGI0ZTQ3YgBGAAAAAADFYQb3DJ_xSJHh14kbXHWhBwB08dwEuoS_QYSBDzuv558sAAAAAAENAAB08dwEuoS_QYSBDzuv558sAACGVOwUAAA%3D") + .to_return(body: File.read("./spec/fixtures/events/o365/create.json")) + + WebMock.stub(:patch, "https://graph.microsoft.com/v1.0/users/dev%40acaprojects.onmicrosoft.com/calendar/events/AAMkADE3YmQxMGQ2LTRmZDgtNDljYy1hNDg1LWM0NzFmMGI0ZTQ3YgBGAAAAAADFYQb3DJ_xSJHh14kbXHWhBwB08dwEuoS_QYSBDzuv558sAAAAAAENAAB08dwEuoS_QYSBDzuv558sAACGVOwUAAA%3D") + .to_return(body: File.read("./spec/fixtures/events/o365/update.json")) + + WebMock.stub(:get, "https://graph.microsoft.com/v1.0/users/dev%40acaprojects.com/calendars") + .to_return(body: File.read("./spec/fixtures/calendars/o365/show.json")) + + # Stub getting the host event + WebMock.stub(:get, "https://graph.microsoft.com/v1.0/users/dev%40acaprojects.onmicrosoft.com/calendar/calendarView?startDateTime=2020-08-26T14%3A00%3A00-00%3A00&endDateTime=2020-08-27T13%3A59%3A59-00%3A00&%24filter=iCalUId+eq+%27040000008200E00074C5B7101A82E008000000006DE2E3761F8AD6010000000000000000100000009CCCDBB1F09DE74D8B157797D97F6A10%27&%24top=10000") + .to_return(body: File.read("./spec/fixtures/events/o365/events_query.json")) + + req_body = EventsHelper.create_event_input(permission: PlaceOS::Model::EventMetadata::Permission::PRIVATE) + + event = JSON.parse(client.post(EVENTS_BASE, headers: headers, body: req_body).body).as_h + event_id = event["id"].to_s + + WebMock.stub(:get, "https://graph.microsoft.com/v1.0/users/room1%40example.com/calendar/calendarView?startDateTime=2020-08-26T14:00:00-00:00&endDateTime=2020-08-27T13:59:59-00:00&%24filter=iCalUId+eq+%27040000008200E00074C5B7101A82E008000000006DE2E3761F8AD6010000000000000000100000009CCCDBB1F09DE74D8B157797D97F6A10%27&$top=10000") + .to_return(EventsHelper.event_query_response(event_id)) + + system_id = "sys-rJQQlR4Cn7" + EventsHelper.stub_permissions_check(system_id) + + # public user + no_auth_headers = Mock::Headers.office365_no_auth + response = client.post(%(#{EVENTS_BASE}/#{event_id}/attendee?system_id=#{system_id}), headers: no_auth_headers, body: { + name: "User Two", + email: "user-two@example.com", + checked_in: true, + visit_expected: true, + }.to_json) + response.status_code.should eq(401) + + # same tenant user + same_tenant_headers = Mock::Headers.office365_normal_user(email: "user-three@example.com") + response = client.post(%(#{EVENTS_BASE}/#{event_id}/attendee?system_id=#{system_id}), headers: same_tenant_headers, body: { + name: "User Three", + email: "user-three@example.com", + checked_in: true, + visit_expected: true, + }.to_json) + response.status_code.should eq(403) + + event_metadata = EventMetadata.find_by(event_id: event_id) + # Should only have the event creator and room + event_metadata.attendees.count.should eq(2) + + guests = event_metadata.attendees.map(&.guest.not_nil!) + (guests.map(&.email) - ["jon@example.com", "dev@acaprojects.onmicrosoft.com"]).size.should eq(0) + end + + it "#add_attendee should allow adding same tenant users to OPEN events" do + WebMock.stub(:post, "https://graph.microsoft.com/v1.0/users/dev%40acaprojects.onmicrosoft.com/calendar/events") + .to_return(body: File.read("./spec/fixtures/events/o365/create.json")) + + WebMock.stub(:get, "https://graph.microsoft.com/v1.0/users/room1%40example.com/calendar/events/AAMkADE3YmQxMGQ2LTRmZDgtNDljYy1hNDg1LWM0NzFmMGI0ZTQ3YgBGAAAAAADFYQb3DJ_xSJHh14kbXHWhBwB08dwEuoS_QYSBDzuv558sAAAAAAENAAB08dwEuoS_QYSBDzuv558sAACGVOwUAAA%3D") + .to_return(body: File.read("./spec/fixtures/events/o365/create.json")) + + WebMock.stub(:patch, "https://graph.microsoft.com/v1.0/users/dev%40acaprojects.onmicrosoft.com/calendar/events/AAMkADE3YmQxMGQ2LTRmZDgtNDljYy1hNDg1LWM0NzFmMGI0ZTQ3YgBGAAAAAADFYQb3DJ_xSJHh14kbXHWhBwB08dwEuoS_QYSBDzuv558sAAAAAAENAAB08dwEuoS_QYSBDzuv558sAACGVOwUAAA%3D") + .to_return(body: File.read("./spec/fixtures/events/o365/update.json")) + + WebMock.stub(:get, "https://graph.microsoft.com/v1.0/users/dev%40acaprojects.com/calendars") + .to_return(body: File.read("./spec/fixtures/calendars/o365/show.json")) + + # Stub getting the host event + WebMock.stub(:get, "https://graph.microsoft.com/v1.0/users/dev%40acaprojects.onmicrosoft.com/calendar/calendarView?startDateTime=2020-08-26T14%3A00%3A00-00%3A00&endDateTime=2020-08-27T13%3A59%3A59-00%3A00&%24filter=iCalUId+eq+%27040000008200E00074C5B7101A82E008000000006DE2E3761F8AD6010000000000000000100000009CCCDBB1F09DE74D8B157797D97F6A10%27&%24top=10000") + .to_return(body: File.read("./spec/fixtures/events/o365/events_query.json")) + + req_body = EventsHelper.create_event_input(permission: PlaceOS::Model::EventMetadata::Permission::OPEN) + + event = JSON.parse(client.post(EVENTS_BASE, headers: headers, body: req_body).body).as_h + event_id = event["id"].to_s + + WebMock.stub(:get, "https://graph.microsoft.com/v1.0/users/room1%40example.com/calendar/calendarView?startDateTime=2020-08-26T14:00:00-00:00&endDateTime=2020-08-27T13:59:59-00:00&%24filter=iCalUId+eq+%27040000008200E00074C5B7101A82E008000000006DE2E3761F8AD6010000000000000000100000009CCCDBB1F09DE74D8B157797D97F6A10%27&$top=10000") + .to_return(EventsHelper.event_query_response(event_id)) + + system_id = "sys-rJQQlR4Cn7" + EventsHelper.stub_permissions_check(system_id) + + # public user + no_auth_headers = Mock::Headers.office365_no_auth + response = client.post(%(#{EVENTS_BASE}/#{event_id}/attendee?system_id=#{system_id}), headers: no_auth_headers, body: { + name: "User Two", + email: "user-two@example.com", + checked_in: true, + visit_expected: true, + }.to_json) + response.status_code.should eq(401) + + # same tenant user + same_tenant_headers = Mock::Headers.office365_normal_user(email: "user-three@example.com") + response = client.post(%(#{EVENTS_BASE}/#{event_id}/attendee?system_id=#{system_id}), headers: same_tenant_headers, body: { + name: "User Three", + email: "user-three@example.com", + checked_in: true, + visit_expected: true, + }.to_json) + response.status_code.should eq(200) + + event_metadata = EventMetadata.find_by(event_id: event_id) + event_metadata.attendees.count.should eq(3) + + guests = event_metadata.attendees.map(&.guest.not_nil!) + (guests.map(&.email) - [ + "jon@example.com", + "dev@acaprojects.onmicrosoft.com", + "user-three@example.com", + ]).size.should eq(0) + end + + it "#add_attendee should allow adding anyone to PUBLIC events" do + WebMock.stub(:post, "https://graph.microsoft.com/v1.0/users/dev%40acaprojects.onmicrosoft.com/calendar/events") + .to_return(body: File.read("./spec/fixtures/events/o365/create.json")) + + WebMock.stub(:get, "https://graph.microsoft.com/v1.0/users/room1%40example.com/calendar/events/AAMkADE3YmQxMGQ2LTRmZDgtNDljYy1hNDg1LWM0NzFmMGI0ZTQ3YgBGAAAAAADFYQb3DJ_xSJHh14kbXHWhBwB08dwEuoS_QYSBDzuv558sAAAAAAENAAB08dwEuoS_QYSBDzuv558sAACGVOwUAAA%3D") + .to_return(body: File.read("./spec/fixtures/events/o365/create.json")) + + WebMock.stub(:patch, "https://graph.microsoft.com/v1.0/users/dev%40acaprojects.onmicrosoft.com/calendar/events/AAMkADE3YmQxMGQ2LTRmZDgtNDljYy1hNDg1LWM0NzFmMGI0ZTQ3YgBGAAAAAADFYQb3DJ_xSJHh14kbXHWhBwB08dwEuoS_QYSBDzuv558sAAAAAAENAAB08dwEuoS_QYSBDzuv558sAACGVOwUAAA%3D") + .to_return(body: File.read("./spec/fixtures/events/o365/update.json")) + + WebMock.stub(:get, "https://graph.microsoft.com/v1.0/users/dev%40acaprojects.com/calendars") + .to_return(body: File.read("./spec/fixtures/calendars/o365/show.json")) + + # Stub getting the host event + WebMock.stub(:get, "https://graph.microsoft.com/v1.0/users/dev%40acaprojects.onmicrosoft.com/calendar/calendarView?startDateTime=2020-08-26T14%3A00%3A00-00%3A00&endDateTime=2020-08-27T13%3A59%3A59-00%3A00&%24filter=iCalUId+eq+%27040000008200E00074C5B7101A82E008000000006DE2E3761F8AD6010000000000000000100000009CCCDBB1F09DE74D8B157797D97F6A10%27&%24top=10000") + .to_return(body: File.read("./spec/fixtures/events/o365/events_query.json")) + + req_body = EventsHelper.create_event_input(permission: PlaceOS::Model::EventMetadata::Permission::PUBLIC) + + event = JSON.parse(client.post(EVENTS_BASE, headers: headers, body: req_body).body).as_h + event_id = event["id"].to_s + + WebMock.stub(:get, "https://graph.microsoft.com/v1.0/users/room1%40example.com/calendar/calendarView?startDateTime=2020-08-26T14:00:00-00:00&endDateTime=2020-08-27T13:59:59-00:00&%24filter=iCalUId+eq+%27040000008200E00074C5B7101A82E008000000006DE2E3761F8AD6010000000000000000100000009CCCDBB1F09DE74D8B157797D97F6A10%27&$top=10000") + .to_return(EventsHelper.event_query_response(event_id)) + + system_id = "sys-rJQQlR4Cn7" + EventsHelper.stub_permissions_check(system_id) + + # public user + no_auth_headers = Mock::Headers.office365_no_auth + response = client.post(%(#{EVENTS_BASE}/#{event_id}/attendee?system_id=#{system_id}), headers: no_auth_headers, body: { + name: "User Two", + email: "user-two@example.com", + checked_in: true, + visit_expected: true, + }.to_json) + response.status_code.should eq(200) + + # same tenant user + same_tenant_headers = Mock::Headers.office365_normal_user(email: "user-three@example.com") + response = client.post(%(#{EVENTS_BASE}/#{event_id}/attendee?system_id=#{system_id}), headers: same_tenant_headers, body: { + name: "User Three", + email: "user-three@example.com", + checked_in: true, + visit_expected: true, + }.to_json) + response.status_code.should eq(200) + + event_metadata = EventMetadata.find_by(event_id: event_id) + event_metadata.attendees.count.should eq(4) + + guests = event_metadata.attendees.map(&.guest.not_nil!) + (guests.map(&.email) - [ + "jon@example.com", + "dev@acaprojects.onmicrosoft.com", + "user-two@example.com", + "user-three@example.com", + ]).size.should eq(0) + end + + pending "#index should return a list of PUBLIC events for unauthenticated users" do + WebMock.stub(:get, "https://graph.microsoft.com/v1.0/users/dev%40acaprojects.com/calendar?") + .to_return(body: File.read("./spec/fixtures/calendars/o365/show.json")) + WebMock.stub(:get, "#{ENV["PLACE_URI"]}/api/engine/v2/systems?limit=1000&offset=0&zone_id=z1") + .to_return(body: File.read("./spec/fixtures/placeos/systems.json")) + WebMock.stub(:post, "https://graph.microsoft.com/v1.0/%24batch") + .to_return(body: File.read("./spec/fixtures/events/o365/batch_index.json")) + + tenant = get_tenant + + # private booking + private_event = EventMetadatasHelper.create_event( + tenant.id, + event_start: 10.minutes.from_now.to_unix, + event_end: 50.minutes.from_now.to_unix, + permission: PlaceOS::Model::EventMetadata::Permission::PRIVATE, + ) + + # open booking + open_event = EventMetadatasHelper.create_event( + tenant.id, + event_start: 10.minutes.from_now.to_unix, + event_end: 50.minutes.from_now.to_unix, + permission: PlaceOS::Model::EventMetadata::Permission::OPEN, + ) + + # public booking + public_event = EventMetadatasHelper.create_event( + tenant.id, + event_start: 10.minutes.from_now.to_unix, + event_end: 50.minutes.from_now.to_unix, + permission: PlaceOS::Model::EventMetadata::Permission::PUBLIC, + ) + + starting = 5.minutes.from_now.to_unix + ending = 90.minutes.from_now.to_unix + + # public user + no_auth_headers = Mock::Headers.office365_no_auth + response = client.get("#{EVENTS_BASE}?zone_ids=z1&period_start=#{starting}&period_end=#{starting}", headers: no_auth_headers) + response.status_code.should eq(200) + events = JSON.parse(response.body).as_a + events.size.should eq(1) + events.map(&.["id"]).should_not contain(private_event.id) + events.map(&.["id"]).should_not contain(open_event.id) + events.map(&.["id"]).should contain(public_event.id) + end + + pending "#index should return a list of OPEN and PUBLIC events for same tenant users" do + WebMock.stub(:get, "https://graph.microsoft.com/v1.0/users/dev%40acaprojects.com/calendar?") + .to_return(body: File.read("./spec/fixtures/calendars/o365/show.json")) + WebMock.stub(:get, "#{ENV["PLACE_URI"]}/api/engine/v2/systems?limit=1000&offset=0&zone_id=z1") + .to_return(body: File.read("./spec/fixtures/placeos/systems.json")) + WebMock.stub(:post, "https://graph.microsoft.com/v1.0/%24batch") + .to_return(body: File.read("./spec/fixtures/events/o365/batch_index.json")) + + tenant = get_tenant + + # private booking + private_event = EventMetadatasHelper.create_event( + tenant.id, + event_start: 10.minutes.from_now.to_unix, + event_end: 50.minutes.from_now.to_unix, + permission: PlaceOS::Model::EventMetadata::Permission::PRIVATE, + ) + + # open booking + open_event = EventMetadatasHelper.create_event( + tenant.id, + event_start: 10.minutes.from_now.to_unix, + event_end: 50.minutes.from_now.to_unix, + permission: PlaceOS::Model::EventMetadata::Permission::OPEN, + ) + + # public booking + public_event = EventMetadatasHelper.create_event( + tenant.id, + event_start: 10.minutes.from_now.to_unix, + event_end: 50.minutes.from_now.to_unix, + permission: PlaceOS::Model::EventMetadata::Permission::PUBLIC, + ) + + starting = 5.minutes.from_now.to_unix + ending = 90.minutes.from_now.to_unix + + # same tenant user + same_tenant_headers = Mock::Headers.office365_normal_user(email: "user-four@example.com") + response = client.get("#{EVENTS_BASE}?zone_ids=z1&period_start=#{starting}&period_end=#{starting}", headers: same_tenant_headers) + response.status_code.should eq(200) + events = JSON.parse(response.body).as_a + events.size.should eq(1) + events.map(&.["id"]).should_not contain(private_event.id) + events.map(&.["id"]).should contain(open_event.id) + events.map(&.["id"]).should contain(public_event.id) + end + + pending "#index should return a list of PRIVATE, OPEN, and PUBLIC group-event events for the event creator" do + end + + pending "#index should NOT include attendee details for unauthenticated users" do + end + + pending "#destroy_attendee should allow same tenant users to remove attendees from OPEN events" do + end + end + describe "#show" do before_each do EventsHelper.stub_show_endpoints diff --git a/spec/controllers/helpers/event_helper.cr b/spec/controllers/helpers/event_helper.cr index 9cd4085e..664bf3c2 100644 --- a/spec/controllers/helpers/event_helper.cr +++ b/spec/controllers/helpers/event_helper.cr @@ -81,7 +81,7 @@ module EventsHelper }.to_json end - def create_event_input(user = Mock::Token.generate_auth_user(false, false)) + def create_event_input(user = Mock::Token.generate_auth_user(false, false), permission = PlaceOS::Model::EventMetadata::Permission::PRIVATE) %({ "event_start": 1598503500, "event_end": 1598507160, @@ -125,6 +125,7 @@ module EventsHelper "system": { "id": "sys-rJQQlR4Cn7" }, + "permission": "#{permission}", "extension_data": { "foo": "bar" } diff --git a/spec/spec_helper.cr b/spec/spec_helper.cr index 800398a6..20759d25 100644 --- a/spec/spec_helper.cr +++ b/spec/spec_helper.cr @@ -235,7 +235,8 @@ module EventMetadatasHelper room_email = Faker::Internet.email, host = Faker::Internet.email, ext_data = JSON.parse({"foo": 123}.to_json), - ical_uid = "random_uid-#{Random.new.rand(500)}") + ical_uid = "random_uid-#{Random.new.rand(500)}", + permission = PlaceOS::Model::EventMetadata::Permission::PRIVATE) EventMetadata.create!( tenant_id: tenant_id, system_id: system_id, @@ -246,6 +247,7 @@ module EventMetadatasHelper event_end: event_end, ext_data: ext_data, ical_uid: ical_uid, + permission: permission ) end end diff --git a/src/controllers/events.cr b/src/controllers/events.cr index 505c7b64..f58b8a12 100644 --- a/src/controllers/events.cr +++ b/src/controllers/events.cr @@ -2,7 +2,28 @@ class Events < Application base "/api/staff/v1/events" # Skip scope check for relevant routes - skip_action :check_jwt_scope, only: [:show, :patch_metadata, :guest_checkin] + skip_action :check_jwt_scope, only: [:show, :patch_metadata, :guest_checkin, :add_attendee] + + # Skip actions that requres login + # If a user is logged in then they will be run as part of + # #set_tenant_from_domain + skip_action :determine_tenant_from_domain, only: [:add_attendee] + + # Set the tenant based on the domain + # This allows unauthenticated requests through + # (for public bookings, further checks are done later) + @[AC::Route::Filter(:before_action, only: [:add_attendee])] + private def set_tenant_from_domain + if auth_token_present? + check_jwt_scope + determine_tenant_from_domain + else + domain = request.hostname.as?(String) + raise Error::BadRequest.new("missing domain header") unless domain + @tenant = Tenant.find_by?(domain: domain) + raise Error::NotFound.new("could not find tenant with domain: #{domain}") unless tenant + end + end @[AC::Route::Filter(:before_action, only: [:notify_change, :link_master_metadata])] private def protected_route @@ -23,6 +44,25 @@ class Events < Application raise Error::Forbidden.new("user not in an appropriate user group or involved in the meeting") end + private def confirm_access_for_add_attendee( + event : PlaceCalendar::Event, + metadata : EventMetadata, + system : PlaceOS::Client::API::Models::System? = nil, + ) + return if metadata.permission.public? + return if is_support? + + if user = current_user + return if event && (event_creator = event.creator) && (event_creator.downcase == user.email.downcase) + if system + return if check_access(current_user.groups, system.zones || [] of String).can_manage? + end + return if metadata.permission.open? && (authority = user.authority) && (event_tenant = metadata.tenant) && (authority.domain == event_tenant.domain) + end + + raise Error::Forbidden.new("user not in an appropriate user group or involved in the meeting") + end + # update includes a bunch of moving parts so we want to roll back if something fails @[AC::Route::Filter(:around_action, only: [:update])] def wrap_in_transaction(&) @@ -313,6 +353,9 @@ class Events < Application if breakdown_event_id = input_event.breakdown_event_id meta.breakdown_event_id = breakdown_event_id end + if permission = input_event.permission + meta.permission = permission + end notify_created_or_updated(:create, sys, created_event, meta, can_skip: false, is_host: true) if attending && !attending.empty? @@ -550,6 +593,9 @@ class Events < Application if breakdown_event_id = changes.breakdown_event_id meta.breakdown_event_id = breakdown_event_id end + if permission = changes.permission + meta.permission = permission + end notify_created_or_updated(:update, system, updated_event, meta, can_skip: false, is_host: true) # Grab the list of externals that might be attending @@ -690,6 +736,150 @@ class Events < Application end end + # Adds a single attendee to an existing event + @[AC::Route::POST("/:id/attendee", body: :attendee)] + def add_attendee( + @[AC::Param::Info(name: "id", description: "the event id", example: "AAMkAGVmMDEzMTM4LTZmYWUtNDdkNC1hMDZe")] + original_id : String, + attendee : PlaceCalendar::Event::Attendee, + @[AC::Param::Info(description: "the event space associated with this event", example: "sys-1234")] + system_id : String? = nil, + @[AC::Param::Info(name: "calendar", description: "the calendar associated with this event id", example: "user@org.com")] + user_cal : String? = nil + ) : Attendee | PlaceCalendar::Event::Attendee + placeos_client = get_placeos_client + event_id = original_id + email = attendee.email.strip.downcase + cal_id = user_cal.try &.downcase + + if system_id + system = placeos_client.systems.fetch(system_id) + sys_cal = system.email.presence + if cal_id.nil? + cal_id = system.email.presence + raise AC::Route::Param::ValueError.new("system '#{system.name}' (#{system_id}) does not have a resource email address specified", "system_id") unless sys_cal + end + end + + # defaults to the current users email + if auth_token_present? && !cal_id + cal_id = user.email + end + + raise AC::Route::Param::ValueError.new("Either system_id or calendar must be provided") unless cal_id + + event = client.get_event(cal_id, id: event_id, calendar_id: cal_id) + raise Error::NotFound.new("failed to find event #{event_id} searching on #{cal_id} as #{user.email}") unless event + + # ensure we have the host event details + if client.client_id == :office365 && event.host.try(&.downcase) != cal_id + event = get_hosts_event(event) + raise Error::BadUpstreamResponse.new("event id is missing") unless event_id = event.id + end + + # User details + if auth_token_present? + user_email = user.email.downcase + host = event.host.try(&.downcase) || user_email + else + host = event.host.try(&.downcase) + end + raise Error::BadUpstreamResponse.new("event host is missing") unless host + + metadata = get_event_metadata(event, system_id) + raise Error::NotFound.new("metadata not found for event #{event_id}") unless metadata + + # check permisions + confirm_access_for_add_attendee(event, metadata, system) + + # Check if attendee already exists in the event to avoid duplicates + existing_attendee = event.attendees.find { |a| a.email == email } + raise Error::BadRequest.new("Attendee already exists in this booking") if existing_attendee + + # Add the new attendee to the event + event.attendees = (event.attendees || [] of PlaceCalendar::Event::Attendee) << attendee + + # Update the event with the new attendee + updated_event = client.update_event(user_id: host, event: event, calendar_id: host) + raise Error::BadUpstreamResponse.new("failed to update event #{event_id} as #{host}") unless updated_event + + if system_id + meta = get_migrated_metadata(updated_event, system_id) || EventMetadata.new + + email = attendee.email.strip.downcase + + # Create or update the attendee in the metadata + guest = if existing_guest = Guest.by_tenant(tenant.id).find_by?(email: email) + existing_guest + else + Guest.new( + email: email, + name: attendee.name, + preferred_name: attendee.preferred_name, + phone: attendee.phone, + organisation: attendee.organisation, + photo: attendee.photo, + notes: attendee.notes, + banned: attendee.banned || false, + dangerous: attendee.dangerous || false, + tenant_id: tenant.id, + ) + end + + if attendee_ext_data = attendee.extension_data + guest.extension_data = attendee_ext_data + end + + guest.save! + + # Create attendee + attend = existing_attendee || Attendee.new + + previously_visiting = if attend.persisted? + attend.visit_expected + else + attend.assign_attributes( + visit_expected: true, + checked_in: false, + tenant_id: tenant.id, + ) + false + end + + raise Error::InconsistentState.new("metadata id must be present") unless meta_id = meta.id + raise Error::InconsistentState.new("visit_expected must be present") unless attendee_visit_expected = attendee.visit_expected + + attend.update!( + event_id: meta_id, + guest_id: guest.id, + visit_expected: attendee_visit_expected, + ) + + if system && !previously_visiting + spawn do + sys = system + raise Error::BadUpstreamResponse.new("event_start must be present on updated event #{updated_event.id}") unless updated_event_start = updated_event.event_start + + placeos_client.root.signal("staff/guest/attending", { + action: :meeting_update, + system_id: sys.id, + event_id: event_id, + event_ical_uid: updated_event.ical_uid, + host: host, + resource: sys.email, + event_summary: updated_event.title, + event_starting: updated_event_start.to_unix, + attendee_name: attendee.name, + attendee_email: attendee.email, + zones: sys.zones, + }) + end + end + end + + attend || attendee + end + # used to link resource recurring master ids to metadata @[AC::Route::POST("/:id/metadata/:system_id/link/:ical_uid", status_code: HTTP::Status::ACCEPTED)] def link_master_metadata( diff --git a/src/models/event.cr b/src/models/event.cr index 95985b6e..5a49141d 100644 --- a/src/models/event.cr +++ b/src/models/event.cr @@ -53,6 +53,7 @@ class StaffApi::Event event.breakdown_time = metadata.try(&.breakdown_time) event.setup_event_id = metadata.try(&.setup_event_id) event.breakdown_event_id = metadata.try(&.breakdown_event_id) + event.permission = metadata.try(&.permission) end event.recurring_master_id = event.recurring_event_id @@ -88,6 +89,8 @@ class PlaceCalendar::Event property setup_event_id : String? = nil property breakdown_event_id : String? = nil + property permission : PlaceOS::Model::EventMetadata::Permission? = nil + struct Attendee property checked_in : Bool? property visit_expected : Bool?